Browse Source

Tenants and tenant admins pages.

pull/1954/head
Igor Kulikov 7 years ago
parent
commit
08e8c92970
  1. 55
      ui-ngx/src/app/core/http/admin.service.ts
  2. 52
      ui-ngx/src/app/core/http/customer.service.ts
  3. 77
      ui-ngx/src/app/core/http/dashboard.service.ts
  4. 50
      ui-ngx/src/app/core/http/tenant.service.ts
  5. 69
      ui-ngx/src/app/core/services/menu.service.ts
  6. 10
      ui-ngx/src/app/core/translate/translate-default-compiler.ts
  7. 2
      ui-ngx/src/app/modules/home/menu/side-menu.component.scss
  8. 89
      ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts
  9. 39
      ui-ngx/src/app/modules/home/pages/admin/admin.module.ts
  10. 47
      ui-ngx/src/app/modules/home/pages/admin/general-settings.component.html
  11. 18
      ui-ngx/src/app/modules/home/pages/admin/general-settings.component.scss
  12. 77
      ui-ngx/src/app/modules/home/pages/admin/general-settings.component.ts
  13. 104
      ui-ngx/src/app/modules/home/pages/admin/mail-server.component.html
  14. 18
      ui-ngx/src/app/modules/home/pages/admin/mail-server.component.scss
  15. 101
      ui-ngx/src/app/modules/home/pages/admin/mail-server.component.ts
  16. 119
      ui-ngx/src/app/modules/home/pages/admin/security-settings.component.html
  17. 23
      ui-ngx/src/app/modules/home/pages/admin/security-settings.component.scss
  18. 86
      ui-ngx/src/app/modules/home/pages/admin/security-settings.component.ts
  19. 25
      ui-ngx/src/app/modules/home/pages/admin/settings-card.scss
  20. 14
      ui-ngx/src/app/modules/home/pages/home-pages.module.ts
  21. 63
      ui-ngx/src/app/modules/home/pages/profile/change-password-dialog.component.html
  22. 17
      ui-ngx/src/app/modules/home/pages/profile/change-password-dialog.component.scss
  23. 68
      ui-ngx/src/app/modules/home/pages/profile/change-password-dialog.component.ts
  24. 69
      ui-ngx/src/app/modules/home/pages/profile/profile-routing.module.ts
  25. 77
      ui-ngx/src/app/modules/home/pages/profile/profile.component.html
  26. 32
      ui-ngx/src/app/modules/home/pages/profile/profile.component.scss
  27. 123
      ui-ngx/src/app/modules/home/pages/profile/profile.component.ts
  28. 38
      ui-ngx/src/app/modules/home/pages/profile/profile.module.ts
  29. 80
      ui-ngx/src/app/modules/home/pages/tenant/tenant-routing.module.ts
  30. 61
      ui-ngx/src/app/modules/home/pages/tenant/tenant.component.html
  31. 76
      ui-ngx/src/app/modules/home/pages/tenant/tenant.component.ts
  32. 36
      ui-ngx/src/app/modules/home/pages/tenant/tenant.module.ts
  33. 105
      ui-ngx/src/app/modules/home/pages/tenant/tenants-table-config.resolver.ts
  34. 59
      ui-ngx/src/app/modules/home/pages/user/activation-link-dialog.component.html
  35. 64
      ui-ngx/src/app/modules/home/pages/user/activation-link-dialog.component.ts
  36. 59
      ui-ngx/src/app/modules/home/pages/user/add-user-dialog.component.html
  37. 114
      ui-ngx/src/app/modules/home/pages/user/add-user-dialog.component.ts
  38. 28
      ui-ngx/src/app/modules/home/pages/user/user-routing.module.ts
  39. 90
      ui-ngx/src/app/modules/home/pages/user/user.component.html
  40. 35
      ui-ngx/src/app/modules/home/pages/user/user.component.scss
  41. 82
      ui-ngx/src/app/modules/home/pages/user/user.component.ts
  42. 42
      ui-ngx/src/app/modules/home/pages/user/user.module.ts
  43. 230
      ui-ngx/src/app/modules/home/pages/user/users-table-config.resolver.ts
  44. 63
      ui-ngx/src/app/shared/components/contact.component.html
  45. 34
      ui-ngx/src/app/shared/components/contact.component.ts
  46. 291
      ui-ngx/src/app/shared/components/contact.models.ts
  47. 46
      ui-ngx/src/app/shared/components/dashboard-autocomplete.component.html
  48. 226
      ui-ngx/src/app/shared/components/dashboard-autocomplete.component.ts
  49. 56
      ui-ngx/src/app/shared/components/details-panel.component.html
  50. 53
      ui-ngx/src/app/shared/components/details-panel.component.scss
  51. 80
      ui-ngx/src/app/shared/components/details-panel.component.ts
  52. 51
      ui-ngx/src/app/shared/components/entity/add-entity-dialog.component.html
  53. 17
      ui-ngx/src/app/shared/components/entity/add-entity-dialog.component.scss
  54. 103
      ui-ngx/src/app/shared/components/entity/add-entity-dialog.component.ts
  55. 84
      ui-ngx/src/app/shared/components/entity/contact-based.component.ts
  56. 137
      ui-ngx/src/app/shared/components/entity/entities-table-config.models.ts
  57. 182
      ui-ngx/src/app/shared/components/entity/entities-table.component.html
  58. 41
      ui-ngx/src/app/shared/components/entity/entities-table.component.scss
  59. 358
      ui-ngx/src/app/shared/components/entity/entities-table.component.ts
  60. 28
      ui-ngx/src/app/shared/components/entity/entity-component.models.ts
  61. 43
      ui-ngx/src/app/shared/components/entity/entity-details-panel.component.html
  62. 31
      ui-ngx/src/app/shared/components/entity/entity-details-panel.component.scss
  63. 165
      ui-ngx/src/app/shared/components/entity/entity-details-panel.component.ts
  64. 36
      ui-ngx/src/app/shared/components/entity/entity-table-header.component.ts
  65. 110
      ui-ngx/src/app/shared/components/entity/entity.component.ts
  66. 47
      ui-ngx/src/app/shared/components/time/datetime-period.component.html
  67. 26
      ui-ngx/src/app/shared/components/time/datetime-period.component.scss
  68. 137
      ui-ngx/src/app/shared/components/time/datetime-period.component.ts
  69. 54
      ui-ngx/src/app/shared/components/time/timeinterval.component.html
  70. 43
      ui-ngx/src/app/shared/components/time/timeinterval.component.scss
  71. 277
      ui-ngx/src/app/shared/components/time/timeinterval.component.ts
  72. 127
      ui-ngx/src/app/shared/components/time/timewindow-panel.component.html
  73. 63
      ui-ngx/src/app/shared/components/time/timewindow-panel.component.scss
  74. 203
      ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts
  75. 43
      ui-ngx/src/app/shared/components/time/timewindow.component.html
  76. 26
      ui-ngx/src/app/shared/components/time/timewindow.component.scss
  77. 275
      ui-ngx/src/app/shared/components/time/timewindow.component.ts
  78. 6
      ui-ngx/src/app/shared/models/customer.model.ts
  79. 35
      ui-ngx/src/app/shared/models/dashboard.models.ts
  80. 113
      ui-ngx/src/app/shared/models/datasource/entity-datasource.ts
  81. 26
      ui-ngx/src/app/shared/models/id/dashboard-id.ts
  82. 17
      ui-ngx/src/app/shared/models/settings.models.ts
  83. 27
      ui-ngx/src/app/shared/pipe/enum-to-array.pipe.ts
  84. 28
      ui-ngx/src/app/shared/pipe/highlight.pipe.ts
  85. 58
      ui-ngx/src/app/shared/pipe/milliseconds-to-time-string.pipe.ts
  86. 84
      ui-ngx/src/app/shared/shared.module.ts
  87. 3
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  88. 2
      ui-ngx/src/assets/locale/locale.constant-es_ES.json
  89. 130
      ui-ngx/src/assets/locale/locale.constant-fr_FR.json
  90. 6
      ui-ngx/src/assets/locale/locale.constant-ru_RU.json
  91. 26
      ui-ngx/src/assets/locale/locale.constant-tr_TR.json
  92. 6
      ui-ngx/src/assets/locale/locale.constant-uk_UA.json
  93. 16
      ui-ngx/src/assets/locale/locale.constant-zh_CN.json

55
ui-ngx/src/app/core/http/admin.service.ts

@ -0,0 +1,55 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Injectable } from '@angular/core';
import { defaultHttpOptions } from './http-utils';
import { Observable } from 'rxjs/index';
import { HttpClient } from '@angular/common/http';
import {AdminSettings, MailServerSettings, SecuritySettings} from '@shared/models/settings.models';
@Injectable({
providedIn: 'root'
})
export class AdminService {
constructor(
private http: HttpClient
) { }
public getAdminSettings<T>(key: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<AdminSettings<T>> {
return this.http.get<AdminSettings<T>>(`/api/admin/settings/${key}`, defaultHttpOptions(ignoreLoading, ignoreErrors));
}
public saveAdminSettings<T>(adminSettings: AdminSettings<T>,
ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<AdminSettings<T>> {
return this.http.post<AdminSettings<T>>('/api/admin/settings', adminSettings, defaultHttpOptions(ignoreLoading, ignoreErrors));
}
public sendTestMail(adminSettings: AdminSettings<MailServerSettings>,
ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<void> {
return this.http.post<void>('/api/admin/settings/testMail', adminSettings, defaultHttpOptions(ignoreLoading, ignoreErrors));
}
public getSecuritySettings(ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<SecuritySettings> {
return this.http.get<SecuritySettings>(`/api/admin/securitySettings`, defaultHttpOptions(ignoreLoading, ignoreErrors));
}
public saveSecuritySettings(securitySettings: SecuritySettings,
ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<SecuritySettings> {
return this.http.post<SecuritySettings>('/api/admin/securitySettings', securitySettings,
defaultHttpOptions(ignoreLoading, ignoreErrors));
}
}

52
ui-ngx/src/app/core/http/customer.service.ts

@ -0,0 +1,52 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Injectable } from '@angular/core';
import { defaultHttpOptions } from './http-utils';
import { Observable } from 'rxjs/index';
import { HttpClient } from '@angular/common/http';
import { PageLink } from '@shared/models/page/page-link';
import { PageData } from '@shared/models/page/page-data';
import { Customer } from '@shared/models/customer.model';
@Injectable({
providedIn: 'root'
})
export class CustomerService {
constructor(
private http: HttpClient
) { }
public getCustomers(tenantId: string, pageLink: PageLink, ignoreErrors: boolean = false,
ignoreLoading: boolean = false): Observable<PageData<Customer>> {
return this.http.get<PageData<Customer>>(`/api/tenant/${tenantId}/customers${pageLink.toQuery()}`,
defaultHttpOptions(ignoreLoading, ignoreErrors));
}
public getCustomer(customerId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Customer> {
return this.http.get<Customer>(`/api/customer/${customerId}`, defaultHttpOptions(ignoreLoading, ignoreErrors));
}
public saveCustomer(customer: Customer, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Customer> {
return this.http.post<Customer>('/api/customer', customer, defaultHttpOptions(ignoreLoading, ignoreErrors));
}
public deleteCustomer(customerId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false) {
return this.http.delete(`/api/customer/${customerId}`, defaultHttpOptions(ignoreLoading, ignoreErrors));
}
}

77
ui-ngx/src/app/core/http/dashboard.service.ts

@ -0,0 +1,77 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Injectable } from '@angular/core';
import { defaultHttpOptions } from './http-utils';
import { Observable } from 'rxjs/index';
import { HttpClient } from '@angular/common/http';
import { PageLink } from '@shared/models/page/page-link';
import { PageData } from '@shared/models/page/page-data';
import { Tenant } from '@shared/models/tenant.model';
import {DashboardInfo, Dashboard} from '@shared/models/dashboard.models';
import {map} from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class DashboardService {
constructor(
private http: HttpClient
) { }
public getTenantDashboards(pageLink: PageLink, ignoreErrors: boolean = false,
ignoreLoading: boolean = false): Observable<PageData<DashboardInfo>> {
return this.http.get<PageData<DashboardInfo>>(`/api/tenant/dashboards${pageLink.toQuery()}`,
defaultHttpOptions(ignoreLoading, ignoreErrors));
}
public getTenantDashboardsByTenantId(tenantId: string, pageLink: PageLink, ignoreErrors: boolean = false,
ignoreLoading: boolean = false): Observable<PageData<DashboardInfo>> {
return this.http.get<PageData<DashboardInfo>>(`/api/tenant/${tenantId}/dashboards${pageLink.toQuery()}`,
defaultHttpOptions(ignoreLoading, ignoreErrors));
}
public getCustomerDashboards(customerId: string, pageLink: PageLink, ignoreErrors: boolean = false,
ignoreLoading: boolean = false): Observable<PageData<DashboardInfo>> {
return this.http.get<PageData<DashboardInfo>>(`/api/customer/${customerId}/dashboards${pageLink.toQuery()}`,
defaultHttpOptions(ignoreLoading, ignoreErrors)).pipe(
map( dashboards => {
dashboards.data = dashboards.data.filter(dashboard => {
return dashboard.title.toUpperCase().includes(pageLink.textSearch.toUpperCase());
});
return dashboards;
}
));
}
public getDashboard(dashboardId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Dashboard> {
return this.http.get<Dashboard>(`/api/dashboard/${dashboardId}`, defaultHttpOptions(ignoreLoading, ignoreErrors));
}
public getDashboardInfo(dashboardId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<DashboardInfo> {
return this.http.get<DashboardInfo>(`/api/dashboard/info/${dashboardId}`, defaultHttpOptions(ignoreLoading, ignoreErrors));
}
public saveDashboard(dashboard: Dashboard, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Dashboard> {
return this.http.post<Dashboard>('/api/dashboard', dashboard, defaultHttpOptions(ignoreLoading, ignoreErrors));
}
public deleteDashboard(dashboardId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false) {
return this.http.delete(`/api/dashboard/${dashboardId}`, defaultHttpOptions(ignoreLoading, ignoreErrors));
}
}

50
ui-ngx/src/app/core/http/tenant.service.ts

@ -0,0 +1,50 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Injectable } from '@angular/core';
import { defaultHttpOptions } from './http-utils';
import { Observable } from 'rxjs/index';
import { HttpClient } from '@angular/common/http';
import { PageLink } from '@shared/models/page/page-link';
import { PageData } from '@shared/models/page/page-data';
import { Tenant } from '@shared/models/tenant.model';
@Injectable({
providedIn: 'root'
})
export class TenantService {
constructor(
private http: HttpClient
) { }
public getTenants(pageLink: PageLink, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<PageData<Tenant>> {
return this.http.get<PageData<Tenant>>(`/api/tenants${pageLink.toQuery()}`, defaultHttpOptions(ignoreLoading, ignoreErrors));
}
public getTenant(tenantId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Tenant> {
return this.http.get<Tenant>(`/api/tenant/${tenantId}`, defaultHttpOptions(ignoreLoading, ignoreErrors));
}
public saveTenant(tenant: Tenant, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Tenant> {
return this.http.post<Tenant>('/api/tenant', tenant, defaultHttpOptions(ignoreLoading, ignoreErrors));
}
public deleteTenant(tenantId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false) {
return this.http.delete(`/api/tenant/${tenantId}`, defaultHttpOptions(ignoreLoading, ignoreErrors));
}
}

69
ui-ngx/src/app/core/services/menu.service.ts

@ -320,15 +320,78 @@ export class MenuService {
type: 'link',
path: '/home',
icon: 'home'
},
{
name: 'asset.assets',
type: 'link',
path: '/assets',
icon: 'domain'
},
{
name: 'device.devices',
type: 'link',
path: '/devices',
icon: 'devices_other'
},
{
name: 'entity-view.entity-views',
type: 'link',
path: '/entityViews',
icon: 'view_quilt'
},
{
name: 'dashboard.dashboards',
type: 'link',
path: '/dashboards',
icon: 'dashboard'
}
);
// TODO:
return sections;
}
private buildCustomerUserHome(authUser: any): Array<HomeSection> {
const homeSections: Array<HomeSection> = [];
// TODO:
const homeSections: Array<HomeSection> = [
{
name: 'asset.view-assets',
places: [
{
name: 'asset.assets',
icon: 'domain',
path: '/assets'
}
]
},
{
name: 'device.view-devices',
places: [
{
name: 'device.devices',
icon: 'devices_other',
path: '/devices'
}
]
},
{
name: 'entity-view.management',
places: [
{
name: 'entity-view.entity-views',
icon: 'view_quilt',
path: '/entityViews'
}
]
},
{
name: 'dashboard.view-dashboards',
places: [
{
name: 'dashboard.dashboards',
icon: 'dashboard',
path: '/dashboards'
}
]
}
];
return homeSections;
}

10
ui-ngx/src/app/core/translate/translate-default-compiler.ts

@ -56,8 +56,14 @@ export class TranslateDefaultCompiler extends TranslateMessageFormatCompiler {
}
private checkIsPlural(src: string): boolean {
const tokens: any[] = parse(src.replace(/\{\{/g, '{').replace(/\}\}/g, '}'),
{cardinal: [], ordinal: []});
let tokens: any[];
try {
tokens = parse(src.replace(/\{\{/g, '{').replace(/\}\}/g, '}'),
{cardinal: [], ordinal: []});
} catch (e) {
console.warn(`Failed to parse source: ${src}`);
console.error(e);
}
const res = tokens.filter(
(value) => typeof value !== 'string' && value.type === 'plural'
);

2
ui-ngx/src/app/modules/home/menu/side-menu.component.scss

@ -106,7 +106,7 @@
button {
padding: 0 16px 0 32px;
font-weight: 500;
text-transform: none;
text-transform: none !important;
text-rendering: optimizeLegibility;
}
}

89
ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts

@ -0,0 +1,89 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { MailServerComponent } from '@modules/home/pages/admin/mail-server.component';
import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard';
import { Authority } from '@shared/models/authority.enum';
import {GeneralSettingsComponent} from "@modules/home/pages/admin/general-settings.component";
import {SecuritySettingsComponent} from "@modules/home/pages/admin/security-settings.component";
const routes: Routes = [
{
path: 'settings',
data: {
auth: [Authority.SYS_ADMIN],
breadcrumb: {
label: 'admin.system-settings',
icon: 'settings'
}
},
children: [
{
path: '',
redirectTo: 'general',
pathMatch: 'full'
},
{
path: 'general',
component: GeneralSettingsComponent,
canDeactivate: [ConfirmOnExitGuard],
data: {
auth: [Authority.SYS_ADMIN],
title: 'admin.general-settings',
breadcrumb: {
label: 'admin.general',
icon: 'settings_applications'
}
}
},
{
path: 'outgoing-mail',
component: MailServerComponent,
canDeactivate: [ConfirmOnExitGuard],
data: {
auth: [Authority.SYS_ADMIN],
title: 'admin.outgoing-mail-settings',
breadcrumb: {
label: 'admin.outgoing-mail',
icon: 'mail'
}
}
},
{
path: 'security-settings',
component: SecuritySettingsComponent,
canDeactivate: [ConfirmOnExitGuard],
data: {
auth: [Authority.SYS_ADMIN],
title: 'admin.security-settings',
breadcrumb: {
label: 'admin.security-settings',
icon: 'security'
}
}
}
]
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class AdminRoutingModule { }

39
ui-ngx/src/app/modules/home/pages/admin/admin.module.ts

@ -0,0 +1,39 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { AdminRoutingModule } from './admin-routing.module';
import { SharedModule } from '@app/shared/shared.module';
import { MailServerComponent } from '@modules/home/pages/admin/mail-server.component';
import {GeneralSettingsComponent} from "@modules/home/pages/admin/general-settings.component";
import {SecuritySettingsComponent} from "@modules/home/pages/admin/security-settings.component";
@NgModule({
declarations:
[
GeneralSettingsComponent,
MailServerComponent,
SecuritySettingsComponent
],
imports: [
CommonModule,
SharedModule,
AdminRoutingModule
]
})
export class AdminModule { }

47
ui-ngx/src/app/modules/home/pages/admin/general-settings.component.html

@ -0,0 +1,47 @@
<!--
Copyright © 2016-2019 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div>
<mat-card class="settings-card">
<mat-card-title>
<div fxLayout="row">
<span class="mat-headline" translate>admin.general-settings</span>
</div>
</mat-card-title>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<mat-card-content style="padding-top: 16px;">
<form #generalSettingsForm="ngForm" [formGroup]="generalSettings" (ngSubmit)="save()">
<fieldset [disabled]="isLoading$ | async">
<mat-form-field class="mat-block">
<mat-label translate>admin.base-url</mat-label>
<input matInput formControlName="baseUrl" required/>
<mat-error *ngIf="generalSettings.get('baseUrl').hasError('required')">
{{ 'admin.base-url-required' | translate }}
</mat-error>
</mat-form-field>
<div fxLayout="row" fxLayoutAlign="end center" style="width: 100%;" class="layout-wrap">
<button mat-button mat-raised-button color="primary" [disabled]="(isLoading$ | async) || generalSettingsForm.invalid || !generalSettingsForm.dirty"
type="submit">{{'action.save' | translate}}
</button>
</div>
</fieldset>
</form>
</mat-card-content>
</mat-card>
</div>

18
ui-ngx/src/app/modules/home/pages/admin/general-settings.component.scss

@ -0,0 +1,18 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
}

77
ui-ngx/src/app/modules/home/pages/admin/general-settings.component.ts

@ -0,0 +1,77 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { PageComponent } from '@shared/components/page.component';
import { Router } from '@angular/router';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import {AdminSettings, GeneralSettings} from '@shared/models/settings.models';
import { AdminService } from '@core/http/admin.service';
import { ActionNotificationShow } from '@core/notification/notification.actions';
import { TranslateService } from '@ngx-translate/core';
import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard';
@Component({
selector: 'tb-general-settings',
templateUrl: './general-settings.component.html',
styleUrls: ['./general-settings.component.scss', './settings-card.scss']
})
export class GeneralSettingsComponent extends PageComponent implements OnInit, HasConfirmForm {
generalSettings: FormGroup;
adminSettings: AdminSettings<GeneralSettings>;
constructor(protected store: Store<AppState>,
private router: Router,
private adminService: AdminService,
private translate: TranslateService,
public fb: FormBuilder) {
super(store);
}
ngOnInit() {
this.buildGeneralServerSettingsForm();
this.adminService.getAdminSettings<GeneralSettings>('general').subscribe(
(adminSettings) => {
this.adminSettings = adminSettings;
this.generalSettings.reset(this.adminSettings.jsonValue);
}
);
}
buildGeneralServerSettingsForm() {
this.generalSettings = this.fb.group({
baseUrl: ['', [Validators.required]]
});
}
save(): void {
this.adminSettings.jsonValue = {...this.adminSettings.jsonValue, ...this.generalSettings.value};
this.adminService.saveAdminSettings(this.adminSettings).subscribe(
(adminSettings) => {
this.adminSettings = adminSettings;
this.generalSettings.reset(this.adminSettings.jsonValue);
}
);
}
confirmForm(): FormGroup {
return this.generalSettings;
}
}

104
ui-ngx/src/app/modules/home/pages/admin/mail-server.component.html

@ -0,0 +1,104 @@
<!--
Copyright © 2016-2019 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div>
<mat-card class="settings-card">
<mat-card-title>
<div fxLayout="row">
<span class="mat-headline" translate>admin.outgoing-mail-settings</span>
<span fxFlex></span>
<div tb-help="outgoingMailSettings"></div>
</div>
</mat-card-title>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<mat-card-content style="padding-top: 16px;">
<form #mailSettingsForm="ngForm" [formGroup]="mailSettings" (ngSubmit)="save()">
<fieldset [disabled]="isLoading$ | async">
<mat-form-field class="mat-block">
<mat-label translate>admin.mail-from</mat-label>
<input matInput formControlName="mailFrom" required/>
<mat-error *ngIf="mailSettings.get('mailFrom').hasError('required')">
{{ 'admin.mail-from-required' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>admin.smtp-protocol</mat-label>
<mat-select matInput formControlName="smtpProtocol">
<mat-option *ngFor="let protocol of smtpProtocols" [value]="protocol">
{{protocol.toUpperCase()}}
</mat-option>
</mat-select>
</mat-form-field>
<div fxLayout.gt-sm="row" fxLayoutGap.gt-sm="10px">
<mat-form-field class="mat-block" fxFlex="100" fxFlex.gt-sm="60">
<mat-label translate>admin.smtp-host</mat-label>
<input matInput formControlName="smtpHost" placeholder="localhost" required/>
<mat-error *ngIf="mailSettings.get('smtpHost').hasError('required')">
{{ 'admin.smtp-host-required' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block" fxFlex="100" fxFlex.gt-sm="40">
<mat-label translate>admin.smtp-port</mat-label>
<input matInput #smtpPortInput formControlName="smtpPort" placeholder="25" maxlength="5" required/>
<mat-hint align="end">{{smtpPortInput.value?.length || 0}}/5</mat-hint>
<mat-error *ngIf="mailSettings.get('smtpPort').hasError('required')">
{{ 'admin.smtp-port-required' | translate }}
</mat-error>
<mat-error *ngIf="mailSettings.get('smtpPort').hasError('pattern') || mailSettings.get('smtpPort').hasError('maxlength')">
{{ 'admin.smtp-port-invalid' | translate }}
</mat-error>
</mat-form-field>
</div>
<mat-form-field class="mat-block">
<mat-label translate>admin.timeout-msec</mat-label>
<input matInput #timeoutInput formControlName="timeout" placeholder="10000" maxlength="6" required/>
<mat-hint align="end">{{timeoutInput.value?.length || 0}}/6</mat-hint>
<mat-error *ngIf="mailSettings.get('timeout').hasError('required')">
{{ 'admin.timeout-required' | translate }}
</mat-error>
<mat-error *ngIf="mailSettings.get('timeout').hasError('pattern') || mailSettings.get('timeout').hasError('maxlength')">
{{ 'admin.timeout-invalid' | translate }}
</mat-error>
</mat-form-field>
<tb-checkbox formControlName="enableTls" trueValue="true" falseValue="false">
{{ 'admin.enable-tls' | translate }}
</tb-checkbox>
<mat-form-field class="mat-block">
<mat-label translate>common.username</mat-label>
<input matInput formControlName="username" placeholder="{{ 'common.enter-username' | translate }}"/>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>common.password</mat-label>
<input matInput formControlName="password" type="password" placeholder="{{ 'common.enter-password' | translate }}"/>
</mat-form-field>
<div fxLayout="row" fxLayoutAlign="end center" style="width: 100%;" class="layout-wrap">
<button mat-button mat-raised-button
type="button" style="margin-right: 16px;"
[disabled]="(isLoading$ | async) || mailSettingsForm.invalid" (click)="sendTestMail()">
{{'admin.send-test-mail' | translate}}
</button>
<button mat-button mat-raised-button color="primary" [disabled]="(isLoading$ | async) || mailSettingsForm.invalid || !mailSettingsForm.dirty"
type="submit">{{'action.save' | translate}}
</button>
</div>
</fieldset>
</form>
</mat-card-content>
</mat-card>
</div>

18
ui-ngx/src/app/modules/home/pages/admin/mail-server.component.scss

@ -0,0 +1,18 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
}

101
ui-ngx/src/app/modules/home/pages/admin/mail-server.component.ts

@ -0,0 +1,101 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { PageComponent } from '@shared/components/page.component';
import { Router } from '@angular/router';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { AdminSettings, MailServerSettings, smtpPortPattern } from '@shared/models/settings.models';
import { AdminService } from '@core/http/admin.service';
import { ActionNotificationShow } from '@core/notification/notification.actions';
import { TranslateService } from '@ngx-translate/core';
import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard';
@Component({
selector: 'tb-mail-server',
templateUrl: './mail-server.component.html',
styleUrls: ['./mail-server.component.scss', './settings-card.scss']
})
export class MailServerComponent extends PageComponent implements OnInit, HasConfirmForm {
mailSettings: FormGroup;
adminSettings: AdminSettings<MailServerSettings>;
smtpProtocols = ['smtp', 'smtps'];
constructor(protected store: Store<AppState>,
private router: Router,
private adminService: AdminService,
private translate: TranslateService,
public fb: FormBuilder) {
super(store);
}
ngOnInit() {
this.buildMailServerSettingsForm();
this.adminService.getAdminSettings<MailServerSettings>('mail').subscribe(
(adminSettings) => {
this.adminSettings = adminSettings;
this.mailSettings.reset(this.adminSettings.jsonValue);
}
);
}
buildMailServerSettingsForm() {
this.mailSettings = this.fb.group({
mailFrom: ['', [Validators.required]],
smtpProtocol: ['smtp'],
smtpHost: ['localhost', [Validators.required]],
smtpPort: ['25', [Validators.required,
Validators.pattern(smtpPortPattern),
Validators.maxLength(5)]],
timeout: ['10000', [Validators.required,
Validators.pattern(/^[0-9]{1,6}$/),
Validators.maxLength(6)]],
enableTls: ['false'],
username: [''],
password: ['']
});
this.registerDisableOnLoadFormControl(this.mailSettings.get('smtpProtocol'));
this.registerDisableOnLoadFormControl(this.mailSettings.get('enableTls'));
}
sendTestMail(): void {
this.adminSettings.jsonValue = {...this.adminSettings.jsonValue, ...this.mailSettings.value};
this.adminService.sendTestMail(this.adminSettings).subscribe(
() => {
this.store.dispatch(new ActionNotificationShow({ message: this.translate.instant('admin.test-mail-sent'),
type: 'success' }));
}
);
}
save(): void {
this.adminSettings.jsonValue = {...this.adminSettings.jsonValue, ...this.mailSettings.value};
this.adminService.saveAdminSettings(this.adminSettings).subscribe(
(adminSettings) => {
this.adminSettings = adminSettings;
this.mailSettings.reset(this.adminSettings.jsonValue);
}
);
}
confirmForm(): FormGroup {
return this.mailSettings;
}
}

119
ui-ngx/src/app/modules/home/pages/admin/security-settings.component.html

@ -0,0 +1,119 @@
<!--
Copyright © 2016-2019 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div>
<mat-card class="settings-card">
<mat-card-title>
<div fxLayout="row">
<span class="mat-headline" translate>admin.security-settings</span>
<span fxFlex></span>
<div tb-help="securitySettings"></div>
</div>
</mat-card-title>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<mat-card-content style="padding-top: 16px;">
<form #securitySettingsForm="ngForm" [formGroup]="securitySettingsFormGroup" (ngSubmit)="save()">
<fieldset [disabled]="isLoading$ | async">
<mat-expansion-panel [expanded]="true">
<mat-expansion-panel-header>
<mat-panel-title>
<div class="tb-panel-title" translate>admin.password-policy</div>
</mat-panel-title>
</mat-expansion-panel-header>
<section formGroupName="passwordPolicy">
<mat-form-field class="mat-block">
<mat-label translate>admin.minimum-password-length</mat-label>
<input matInput type="number"
formControlName="minimumLength"
step="1"
min="5"
max="50"
required/>
<mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy').get('minimumLength').hasError('required')">
{{ 'admin.minimum-password-length-required' | translate }}
</mat-error>
<mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy').get('minimumLength').hasError('min')">
{{ 'admin.minimum-password-length-range' | translate }}
</mat-error>
<mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy').get('minimumLength').hasError('max')">
{{ 'admin.minimum-password-length-range' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>admin.minimum-uppercase-letters</mat-label>
<input matInput type="number"
formControlName="minimumUppercaseLetters"
step="1"
min="0"/>
<mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy').get('minimumUppercaseLetters').hasError('min')">
{{ 'admin.minimum-uppercase-letters-range' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>admin.minimum-lowercase-letters</mat-label>
<input matInput type="number"
formControlName="minimumLowercaseLetters"
step="1"
min="0"/>
<mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy').get('minimumLowercaseLetters').hasError('min')">
{{ 'admin.minimum-lowercase-letters-range' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>admin.minimum-digits</mat-label>
<input matInput type="number"
formControlName="minimumDigits"
step="1"
min="0"/>
<mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy').get('minimumDigits').hasError('min')">
{{ 'admin.minimum-digits-range' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>admin.minimum-special-characters</mat-label>
<input matInput type="number"
formControlName="minimumSpecialCharacters"
step="1"
min="0"/>
<mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy').get('minimumSpecialCharacters').hasError('min')">
{{ 'admin.minimum-special-characters-range' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>admin.password-expiration-period-days</mat-label>
<input matInput type="number"
formControlName="passwordExpirationPeriodDays"
step="1"
min="0"/>
<mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy').get('passwordExpirationPeriodDays').hasError('min')">
{{ 'admin.password-expiration-period-days-range' | translate }}
</mat-error>
</mat-form-field>
</section>
</mat-expansion-panel>
<div fxLayout="row" fxLayoutAlign="end center" style="width: 100%;" class="layout-wrap">
<button mat-button mat-raised-button color="primary" [disabled]="(isLoading$ | async) || securitySettingsForm.invalid || !securitySettingsForm.dirty"
type="submit">{{'action.save' | translate}}
</button>
</div>
</fieldset>
</form>
</mat-card-content>
</mat-card>
</div>

23
ui-ngx/src/app/modules/home/pages/admin/security-settings.component.scss

@ -0,0 +1,23 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
mat-expansion-panel {
margin-bottom: 16px;
}
.tb-panel-title {
}
}

86
ui-ngx/src/app/modules/home/pages/admin/security-settings.component.ts

@ -0,0 +1,86 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { PageComponent } from '@shared/components/page.component';
import { Router } from '@angular/router';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { SecuritySettings} from '@shared/models/settings.models';
import { AdminService } from '@core/http/admin.service';
import { ActionNotificationShow } from '@core/notification/notification.actions';
import { TranslateService } from '@ngx-translate/core';
import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard';
@Component({
selector: 'tb-security-settings',
templateUrl: './security-settings.component.html',
styleUrls: ['./security-settings.component.scss', './settings-card.scss']
})
export class SecuritySettingsComponent extends PageComponent implements OnInit, HasConfirmForm {
securitySettingsFormGroup: FormGroup;
securitySettings: SecuritySettings;
constructor(protected store: Store<AppState>,
private router: Router,
private adminService: AdminService,
private translate: TranslateService,
public fb: FormBuilder) {
super(store);
}
ngOnInit() {
this.buildSecuritySettingsForm();
this.adminService.getSecuritySettings().subscribe(
(securitySettings) => {
this.securitySettings = securitySettings;
this.securitySettingsFormGroup.reset(this.securitySettings);
}
);
}
buildSecuritySettingsForm() {
this.securitySettingsFormGroup = this.fb.group({
passwordPolicy: this.fb.group(
{
minimumLength: [null, [Validators.required, Validators.min(5), Validators.max(50)]],
minimumUppercaseLetters: [null, Validators.min(0)],
minimumLowercaseLetters: [null, Validators.min(0)],
minimumDigits: [null, Validators.min(0)],
minimumSpecialCharacters: [null, Validators.min(0)],
passwordExpirationPeriodDays: [null, Validators.min(0)]
}
)
});
}
save(): void {
this.securitySettings = {...this.securitySettings, ...this.securitySettingsFormGroup.value};
this.adminService.saveSecuritySettings(this.securitySettings).subscribe(
(securitySettings) => {
this.securitySettings = securitySettings;
this.securitySettingsFormGroup.reset(this.securitySettings);
}
);
}
confirmForm(): FormGroup {
return this.securitySettingsFormGroup;
}
}

25
ui-ngx/src/app/modules/home/pages/admin/settings-card.scss

@ -0,0 +1,25 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@import "../../../../../scss/constants";
:host {
mat-card.settings-card {
margin: 8px;
@media #{$mat-gt-sm} {
width: 60%;
}
}
}

14
ui-ngx/src/app/modules/home/pages/home-pages.module.ts

@ -16,21 +16,23 @@
import { NgModule } from '@angular/core';
// import { AdminModule } from './admin/admin.module';
import { AdminModule } from './admin/admin.module';
import { HomeLinksModule } from './home-links/home-links.module';
// import { ProfileModule } from './profile/profile.module';
import { ProfileModule } from './profile/profile.module';
import { TenantModule } from '@modules/home/pages/tenant/tenant.module';
// import { CustomerModule } from '@modules/home/pages/customer/customer.module';
// import { AuditLogModule } from '@modules/home/pages/audit-log/audit-log.module';
// import { UserModule } from '@modules/home/pages/user/user.module';
import { UserModule } from '@modules/home/pages/user/user.module';
@NgModule({
exports: [
// AdminModule,
AdminModule,
HomeLinksModule,
// ProfileModule,
ProfileModule,
TenantModule,
// CustomerModule,
// AuditLogModule,
// UserModule
UserModule
]
})
export class HomePagesModule { }

63
ui-ngx/src/app/modules/home/pages/profile/change-password-dialog.component.html

@ -0,0 +1,63 @@
<!--
Copyright © 2016-2019 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<form #changePasswordForm="ngForm" [formGroup]="changePassword" (ngSubmit)="onChangePassword()">
<mat-toolbar fxLayout="row" color="primary">
<h2 translate>profile.change-password</h2>
<span fxFlex></span>
<button mat-button mat-icon-button
[mat-dialog-close]="false"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div mat-dialog-content>
<mat-form-field class="mat-block">
<mat-label translate>profile.current-password</mat-label>
<input matInput type="password" formControlName="currentPassword"/>
<mat-icon class="material-icons" matPrefix>lock</mat-icon>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>login.new-password</mat-label>
<input matInput type="password" formControlName="newPassword"/>
<mat-icon class="material-icons" matPrefix>lock</mat-icon>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>login.new-password-again</mat-label>
<input matInput type="password" formControlName="newPassword2"/>
<mat-icon class="material-icons" matPrefix>lock</mat-icon>
</mat-form-field>
</div>
<div mat-dialog-actions fxLayout="row">
<span fxFlex></span>
<button mat-button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || changePasswordForm.invalid">
{{ 'profile.change-password' | translate }}
</button>
<button mat-button color="primary"
style="margin-right: 20px;"
type="button"
[disabled]="(isLoading$ | async)"
[mat-dialog-close]="false" cdkFocusInitial>
{{ 'action.cancel' | translate }}
</button>
</div>
</form>

17
ui-ngx/src/app/modules/home/pages/profile/change-password-dialog.component.scss

@ -0,0 +1,17 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
}

68
ui-ngx/src/app/modules/home/pages/profile/change-password-dialog.component.ts

@ -0,0 +1,68 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, OnInit } from '@angular/core';
import { MatDialogRef } from '@angular/material';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { ActionNotificationShow } from '@core/notification/notification.actions';
import { TranslateService } from '@ngx-translate/core';
import { AuthService } from '@core/auth/auth.service';
@Component({
selector: 'tb-change-password-dialog',
templateUrl: './change-password-dialog.component.html',
styleUrls: ['./change-password-dialog.component.scss']
})
export class ChangePasswordDialogComponent extends PageComponent implements OnInit {
changePassword: FormGroup;
constructor(protected store: Store<AppState>,
private translate: TranslateService,
private authService: AuthService,
public dialogRef: MatDialogRef<ChangePasswordDialogComponent>,
public fb: FormBuilder) {
super(store);
}
ngOnInit(): void {
this.buildChangePasswordForm();
}
buildChangePasswordForm() {
this.changePassword = this.fb.group({
currentPassword: [''],
newPassword: [''],
newPassword2: ['']
});
}
onChangePassword(): void {
if (this.changePassword.get('newPassword').value !== this.changePassword.get('newPassword2').value) {
this.store.dispatch(new ActionNotificationShow({ message: this.translate.instant('login.passwords-mismatch-error'),
type: 'error' }));
} else {
this.authService.changePassword(
this.changePassword.get('currentPassword').value,
this.changePassword.get('newPassword').value).subscribe(() => {
this.dialogRef.close(true);
});
}
}
}

69
ui-ngx/src/app/modules/home/pages/profile/profile-routing.module.ts

@ -0,0 +1,69 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {Injectable, NgModule} from '@angular/core';
import {Resolve, RouterModule, Routes} from '@angular/router';
import {ProfileComponent} from './profile.component';
import {ConfirmOnExitGuard} from '@core/guards/confirm-on-exit.guard';
import {Authority} from '@shared/models/authority.enum';
import {User} from '@shared/models/user.model';
import {Store} from '@ngrx/store';
import {AppState} from '@core/core.state';
import {UserService} from '@core/http/user.service';
import {getCurrentAuthUser} from '@core/auth/auth.selectors';
import {Observable} from 'rxjs';
@Injectable()
export class UserProfileResolver implements Resolve<User> {
constructor(private store: Store<AppState>,
private userService: UserService) {
}
resolve(): Observable<User> {
const userId = getCurrentAuthUser(this.store).userId;
return this.userService.getUser(userId);
}
}
const routes: Routes = [
{
path: 'profile',
component: ProfileComponent,
canDeactivate: [ConfirmOnExitGuard],
data: {
auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN, Authority.CUSTOMER_USER],
title: 'profile.profile',
breadcrumb: {
label: 'profile.profile',
icon: 'account_circle'
}
},
resolve: {
user: UserProfileResolver
}
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
providers: [
UserProfileResolver
]
})
export class ProfileRoutingModule { }

77
ui-ngx/src/app/modules/home/pages/profile/profile.component.html

@ -0,0 +1,77 @@
<!--
Copyright © 2016-2019 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div>
<mat-card class="profile-card">
<mat-card-title>
<div fxLayout="column">
<span class="mat-headline" translate>profile.profile</span>
<span class="profile-email" style='opacity: 0.7;'>{{ profile ? profile.get('email').value : '' }}</span>
</div>
</mat-card-title>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<mat-card-content style="padding-top: 16px;">
<form #profileForm="ngForm" [formGroup]="profile" (ngSubmit)="save()">
<fieldset [disabled]="isLoading$ | async">
<mat-form-field class="mat-block">
<mat-label translate>user.email</mat-label>
<input matInput formControlName="email" required/>
<mat-error *ngIf="profile.get('email').hasError('required')">
{{ 'user.email-required' | translate }}
</mat-error>
<mat-error *ngIf="profile.get('email').hasError('email')">
{{ 'user.invalid-email-format' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>user.first-name</mat-label>
<input matInput formControlName="firstName"/>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>user.last-name</mat-label>
<input matInput formControlName="lastName"/>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>language.language</mat-label>
<mat-select matInput formControlName="language">
<mat-option *ngFor="let lang of languageList" [value]="lang">
{{ lang ? ('language.locales.' + lang | translate) : ''}}
</mat-option>
</mat-select>
</mat-form-field>
<div fxLayout="row" style="padding-bottom: 16px;">
<button mat-button mat-raised-button color="primary"
type="button"
[disabled]="(isLoading$ | async)" (click)="changePassword()">
{{'profile.change-password' | translate}}
</button>
</div>
<div fxLayout="row" class="layout-wrap">
<span fxFlex></span>
<button mat-button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || profileForm.invalid || !profileForm.dirty">
{{ 'action.save' | translate }}
</button>
</div>
</fieldset>
</form>
</mat-card-content>
</mat-card>
</div>

32
ui-ngx/src/app/modules/home/pages/profile/profile.component.scss

@ -0,0 +1,32 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@import "../../../../../scss/constants";
:host {
mat-card.profile-card {
margin: 8px;
@media #{$mat-gt-sm} {
width: 60%;
}
.mat-headline {
margin: 0;
}
.profile-email {
font-size: 16px;
font-weight: 400;
}
}
}

123
ui-ngx/src/app/modules/home/pages/profile/profile.component.ts

@ -0,0 +1,123 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, OnInit } from '@angular/core';
import { UserService } from '@core/http/user.service';
import { User } from '@shared/models/user.model';
import { Authority } from '@shared/models/authority.enum';
import { PageComponent } from '@shared/components/page.component';
import { select, Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { getCurrentAuthUser, selectAuthUser } from '@core/auth/auth.selectors';
import { mergeMap, take } from 'rxjs/operators';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard';
import { ActionAuthUpdateUserDetails } from '@core/auth/auth.actions';
import { environment as env } from '@env/environment';
import { TranslateService } from '@ngx-translate/core';
import { ActionSettingsChangeLanguage } from '@core/settings/settings.actions';
import { ChangePasswordDialogComponent } from '@modules/home/pages/profile/change-password-dialog.component';
import { MatDialog } from '@angular/material';
import { DialogService } from '@core/services/dialog.service';
import { AuthService } from '@core/auth/auth.service';
import { ActivatedRoute } from '@angular/router';
@Component({
selector: 'tb-profile',
templateUrl: './profile.component.html',
styleUrls: ['./profile.component.scss']
})
export class ProfileComponent extends PageComponent implements OnInit, HasConfirmForm {
authorities = Authority;
profile: FormGroup;
user: User;
languageList = env.supportedLangs;
constructor(protected store: Store<AppState>,
private route: ActivatedRoute,
private userService: UserService,
private authService: AuthService,
private translate: TranslateService,
public dialog: MatDialog,
public dialogService: DialogService,
public fb: FormBuilder) {
super(store);
}
ngOnInit() {
this.buildProfileForm();
this.userLoaded(this.route.snapshot.data.user);
}
buildProfileForm() {
this.profile = this.fb.group({
email: ['', [Validators.required, Validators.email]],
firstName: [''],
lastName: [''],
language: ['']
});
}
save(): void {
this.user = {...this.user, ...this.profile.value};
if (!this.user.additionalInfo) {
this.user.additionalInfo = {};
}
this.user.additionalInfo.lang = this.profile.get('language').value;
this.userService.saveUser(this.user).subscribe(
(user) => {
this.userLoaded(user);
this.store.dispatch(new ActionAuthUpdateUserDetails({ userDetails: {
additionalInfo: {...user.additionalInfo},
authority: user.authority,
createdTime: user.createdTime,
tenantId: user.tenantId,
customerId: user.customerId,
email: user.email,
firstName: user.firstName,
id: user.id,
lastName: user.lastName,
} }));
this.store.dispatch(new ActionSettingsChangeLanguage({ userLang: user.additionalInfo.lang }));
}
);
}
changePassword(): void {
this.dialog.open(ChangePasswordDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog']
});
}
userLoaded(user: User) {
this.user = user;
this.profile.reset(user);
let lang;
if (user.additionalInfo && user.additionalInfo.lang) {
lang = user.additionalInfo.lang;
} else {
lang = this.translate.currentLang;
}
this.profile.get('language').setValue(lang);
}
confirmForm(): FormGroup {
return this.profile;
}
}

38
ui-ngx/src/app/modules/home/pages/profile/profile.module.ts

@ -0,0 +1,38 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ProfileComponent } from './profile.component';
import { SharedModule } from '@shared/shared.module';
import { ProfileRoutingModule } from './profile-routing.module';
import { ChangePasswordDialogComponent } from '@modules/home/pages/profile/change-password-dialog.component';
@NgModule({
entryComponents: [
ChangePasswordDialogComponent
],
declarations: [
ProfileComponent,
ChangePasswordDialogComponent
],
imports: [
CommonModule,
SharedModule,
ProfileRoutingModule
]
})
export class ProfileModule { }

80
ui-ngx/src/app/modules/home/pages/tenant/tenant-routing.module.ts

@ -0,0 +1,80 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Injectable, NgModule } from '@angular/core';
import { Resolve, RouterModule, Routes } from '@angular/router';
import { EntitiesTableComponent } from '@shared/components/entity/entities-table.component';
import { Authority } from '@shared/models/authority.enum';
import { TenantsTableConfigResolver } from '@modules/home/pages/tenant/tenants-table-config.resolver';
import { ProfileComponent } from '@modules/home/pages/profile/profile.component';
import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard';
import { Customer } from '@shared/models/customer.model';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { forkJoin, Observable, throwError } from 'rxjs';
import { getCurrentAuthUser } from '@core/auth/auth.selectors';
import { catchError, finalize, map, tap } from 'rxjs/operators';
import {UsersTableConfigResolver} from '../user/users-table-config.resolver';
const routes: Routes = [
{
path: 'tenants',
data: {
breadcrumb: {
label: 'tenant.tenants',
icon: 'supervisor_account'
}
},
children: [
{
path: '',
component: EntitiesTableComponent,
data: {
auth: [Authority.SYS_ADMIN],
title: 'tenant.tenants'
},
resolve: {
entitiesTableConfig: TenantsTableConfigResolver
}
},
{
path: ':tenantId/users',
component: EntitiesTableComponent,
data: {
auth: [Authority.SYS_ADMIN],
title: 'user.tenant-admins',
breadcrumb: {
label: 'user.tenant-admins',
icon: 'account_circle'
}
},
resolve: {
entitiesTableConfig: UsersTableConfigResolver
}
}
]
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
providers: [
TenantsTableConfigResolver
]
})
export class TenantRoutingModule { }

61
ui-ngx/src/app/modules/home/pages/tenant/tenant.component.html

@ -0,0 +1,61 @@
<!--
Copyright © 2016-2019 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="tb-details-buttons">
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'manageTenantAdmins')"
[fxShow]="!isEdit">
{{'tenant.manage-tenant-admins' | translate }}
</button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'delete')"
[fxShow]="!hideDelete() && !isEdit">
{{'tenant.delete' | translate }}
</button>
<div fxLayout="row">
<button mat-raised-button
ngxClipboard
(cbOnSuccess)="onTenantIdCopied($event)"
[cbContent]="entity?.id?.id"
[fxShow]="!isEdit">
<mat-icon svgIcon="mdi:clipboard-arrow-left"></mat-icon>
<span translate>tenant.copyId</span>
</button>
</div>
</div>
<div class="mat-padding" fxLayout="column">
<form #entityNgForm="ngForm" [formGroup]="entityForm">
<fieldset [disabled]="(isLoading$ | async) || !isEdit">
<mat-form-field class="mat-block">
<mat-label translate>tenant.title</mat-label>
<input matInput formControlName="title" required/>
<mat-error *ngIf="entityForm.get('title').hasError('required')">
{{ 'tenant.title-required' | translate }}
</mat-error>
</mat-form-field>
<div formGroupName="additionalInfo" fxLayout="column">
<mat-form-field class="mat-block">
<mat-label translate>tenant.description</mat-label>
<textarea matInput formControlName="description" rows="2"></textarea>
</mat-form-field>
</div>
<tb-contact [parentForm]="entityForm" [isEdit]="isEdit"></tb-contact>
</fieldset>
</form>
</div>

76
ui-ngx/src/app/modules/home/pages/tenant/tenant.component.ts

@ -0,0 +1,76 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Customer } from '@shared/models/customer.model';
import { ContactBasedComponent } from '@shared/components/entity/contact-based.component';
import {Tenant} from '@app/shared/models/tenant.model';
import {ActionNotificationShow} from '@app/core/notification/notification.actions';
import {TranslateService} from '@ngx-translate/core';
@Component({
selector: 'tb-tenant',
templateUrl: './tenant.component.html'
})
export class TenantComponent extends ContactBasedComponent<Tenant> {
constructor(protected store: Store<AppState>,
protected translate: TranslateService,
protected fb: FormBuilder) {
super(store, fb);
}
hideDelete() {
if (this.entitiesTableConfig) {
return !this.entitiesTableConfig.deleteEnabled(this.entity);
} else {
return false;
}
}
buildEntityForm(entity: Tenant): FormGroup {
return this.fb.group(
{
title: [entity ? entity.title : '', [Validators.required]],
additionalInfo: this.fb.group(
{
description: [entity && entity.additionalInfo ? entity.additionalInfo.description : '']
}
)
}
);
}
updateEntityForm(entity: Tenant) {
this.entityForm.patchValue({title: entity.title});
this.entityForm.patchValue({additionalInfo: {description: entity.additionalInfo ? entity.additionalInfo.description : ''}});
}
onTenantIdCopied(event) {
this.store.dispatch(new ActionNotificationShow(
{
message: this.translate.instant('tenant.idCopiedMessage'),
type: 'success',
duration: 750,
verticalPosition: 'bottom',
horizontalPosition: 'right'
}));
}
}

36
ui-ngx/src/app/modules/home/pages/tenant/tenant.module.ts

@ -0,0 +1,36 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SharedModule } from '@shared/shared.module';
import {TenantComponent} from '@modules/home/pages/tenant/tenant.component';
import {TenantRoutingModule} from '@modules/home/pages/tenant/tenant-routing.module';
@NgModule({
entryComponents: [
TenantComponent
],
declarations: [
TenantComponent
],
imports: [
CommonModule,
SharedModule,
TenantRoutingModule
]
})
export class TenantModule { }

105
ui-ngx/src/app/modules/home/pages/tenant/tenants-table-config.resolver.ts

@ -0,0 +1,105 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Injectable } from '@angular/core';
import { Resolve, Router } from '@angular/router';
import { Tenant } from '@shared/models/tenant.model';
import {
DateEntityTableColumn,
EntityTableColumn,
EntityTableConfig
} from '@shared/components/entity/entities-table-config.models';
import { TenantService } from '@core/http/tenant.service';
import { TranslateService } from '@ngx-translate/core';
import { DatePipe } from '@angular/common';
import {
EntityType,
entityTypeResources,
entityTypeTranslations
} from '@shared/models/entity-type.models';
import { TenantComponent } from '@modules/home/pages/tenant/tenant.component';
import { EntityAction } from '@shared/components/entity/entity-component.models';
import { User } from '@shared/models/user.model';
@Injectable()
export class TenantsTableConfigResolver implements Resolve<EntityTableConfig<Tenant>> {
private readonly config: EntityTableConfig<Tenant> = new EntityTableConfig<Tenant>();
constructor(private tenantService: TenantService,
private translate: TranslateService,
private datePipe: DatePipe,
private router: Router) {
this.config.entityType = EntityType.CUSTOMER;
this.config.entityComponent = TenantComponent;
this.config.entityTranslations = entityTypeTranslations.get(EntityType.TENANT);
this.config.entityResources = entityTypeResources.get(EntityType.TENANT);
this.config.columns.push(
new DateEntityTableColumn<Tenant>('createdTime', 'tenant.created-time', this.datePipe, '150px'),
new EntityTableColumn<Tenant>('title', 'tenant.title'),
new EntityTableColumn<Tenant>('email', 'contact.email'),
new EntityTableColumn<Tenant>('country', 'contact.country'),
new EntityTableColumn<Tenant>('city', 'contact.city')
);
this.config.cellActionDescriptors.push(
{
name: this.translate.instant('tenant.manage-tenant-admins'),
icon: 'account_circle',
isEnabled: () => true,
onAction: ($event, entity) => this.manageTenantAdmins($event, entity)
}
);
this.config.deleteEntityTitle = tenant => this.translate.instant('tenant.delete-tenant-title', { tenantTitle: tenant.title });
this.config.deleteEntityContent = () => this.translate.instant('tenant.delete-tenant-text');
this.config.deleteEntitiesTitle = count => this.translate.instant('tenant.delete-tenants-title', {count});
this.config.deleteEntitiesContent = () => this.translate.instant('tenant.delete-tenants-text');
this.config.entitiesFetchFunction = pageLink => this.tenantService.getTenants(pageLink);
this.config.loadEntity = id => this.tenantService.getTenant(id.id);
this.config.saveEntity = tenant => this.tenantService.saveTenant(tenant);
this.config.deleteEntity = id => this.tenantService.deleteTenant(id.id);
this.config.onEntityAction = action => this.onTenantAction(action);
}
resolve(): EntityTableConfig<Tenant> {
this.config.tableTitle = this.translate.instant('tenant.tenants');
return this.config;
}
manageTenantAdmins($event: Event, tenant: Tenant) {
if ($event) {
$event.stopPropagation();
}
this.router.navigateByUrl(`tenants/${tenant.id.id}/users`);
}
onTenantAction(action: EntityAction<Tenant>): boolean {
switch (action.action) {
case 'manageTenantAdmins':
this.manageTenantAdmins(action.event, action.entity);
return true;
}
return false;
}
}

59
ui-ngx/src/app/modules/home/pages/user/activation-link-dialog.component.html

@ -0,0 +1,59 @@
<!--
Copyright © 2016-2019 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<form style="min-width: 400px;">
<mat-toolbar fxLayout="row" color="primary">
<h2 translate>user.activation-link</h2>
<span fxFlex></span>
<button mat-button mat-icon-button
(click)="close()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div mat-dialog-content tb-toast toastTarget="activationLinkDialogContent">
<div class="mat-content mat-padding" fxLayout="column">
<span [innerHTML]="'user.activation-link-text' | translate: {activationLink: activationLink}"></span>
<div fxLayout="row" fxLayoutAlign="start center">
<pre class="tb-highlight" fxFlex><code>{{ activationLink }}</code></pre>
<button mat-button mat-icon-button
color="primary"
ngxClipboard
cbContent="{{ activationLink }}"
(cbOnSuccess)="onActivationLinkCopied()"
matTooltip="{{ 'user.copy-activation-link' | translate }}"
matTooltipPosition="above">
<mat-icon svgIcon="mdi:clipboard-arrow-left"></mat-icon>
</button>
</div>
</div>
</div>
<div mat-dialog-actions fxLayout="row">
<span fxFlex></span>
<button mat-button color="primary"
style="margin-right: 20px;"
type="button"
cdkFocusInitial
[disabled]="(isLoading$ | async)"
(click)="close()">
{{ 'action.ok' | translate }}
</button>
</div>
</form>

64
ui-ngx/src/app/modules/home/pages/user/activation-link-dialog.component.ts

@ -0,0 +1,64 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, Inject, OnInit } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { TranslateService } from '@ngx-translate/core';
import { ActionNotificationShow } from '@core/notification/notification.actions';
export interface ActivationLinkDialogData {
activationLink: string;
}
@Component({
selector: 'tb-activation-link-dialog',
templateUrl: './activation-link-dialog.component.html'
})
export class ActivationLinkDialogComponent extends PageComponent implements OnInit {
activationLink: string;
constructor(protected store: Store<AppState>,
@Inject(MAT_DIALOG_DATA) public data: ActivationLinkDialogData,
public dialogRef: MatDialogRef<ActivationLinkDialogComponent, void>,
private translate: TranslateService) {
super(store);
this.activationLink = this.data.activationLink;
}
ngOnInit(): void {
}
close(): void {
this.dialogRef.close();
}
onActivationLinkCopied() {
this.store.dispatch(new ActionNotificationShow(
{
message: this.translate.instant('user.activation-link-copied-message'),
type: 'success',
target: 'activationLinkDialogContent',
duration: 1200,
verticalPosition: 'bottom',
horizontalPosition: 'left'
}));
}
}

59
ui-ngx/src/app/modules/home/pages/user/add-user-dialog.component.html

@ -0,0 +1,59 @@
<!--
Copyright © 2016-2019 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<form (ngSubmit)="add()" style="width: 600px;">
<mat-toolbar fxLayout="row" color="primary">
<h2 translate>user.add</h2>
<span fxFlex></span>
<div [tb-help]="'user'"></div>
<button mat-button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div mat-dialog-content>
<tb-user></tb-user>
<mat-form-field class="mat-block">
<mat-label translate>user.activation-method</mat-label>
<mat-select matInput [ngModelOptions]="{standalone: true}" [(ngModel)]="activationMethod">
<mat-option *ngFor="let activationMethod of (activationMethods | enumToArray)" [value]="activationMethods[activationMethod]">
{{ activationMethodTranslations.get(activationMethods[activationMethod]) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div mat-dialog-actions fxLayout="row">
<span fxFlex></span>
<button mat-button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || detailsForm.invalid || !detailsForm.dirty">
{{ 'action.add' | translate }}
</button>
<button mat-button color="primary"
style="margin-right: 20px;"
type="button"
cdkFocusInitial
[disabled]="(isLoading$ | async)"
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
</div>
</form>

114
ui-ngx/src/app/modules/home/pages/user/add-user-dialog.component.ts

@ -0,0 +1,114 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, Inject, OnInit, ViewChild } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { NgForm } from '@angular/forms';
import { UserComponent } from '@modules/home/pages/user/user.component';
import { Authority } from '@shared/models/authority.enum';
import { ActivationMethod, activationMethodTranslations, User } from '@shared/models/user.model';
import { CustomerId } from '@shared/models/id/customer-id';
import { UserService } from '@core/http/user.service';
import { Observable } from 'rxjs';
import {
ActivationLinkDialogComponent,
ActivationLinkDialogData
} from '@modules/home/pages/user/activation-link-dialog.component';
import {TenantId} from '@app/shared/models/id/tenant-id';
export interface AddUserDialogData {
tenantId: string;
customerId: string;
authority: Authority;
}
@Component({
selector: 'tb-add-user-dialog',
templateUrl: './add-user-dialog.component.html'
})
export class AddUserDialogComponent extends PageComponent implements OnInit {
detailsForm: NgForm;
user: User;
activationMethods = ActivationMethod;
activationMethodTranslations = activationMethodTranslations;
activationMethod = ActivationMethod.DISPLAY_ACTIVATION_LINK;
@ViewChild(UserComponent, {static: true}) userComponent: UserComponent;
constructor(protected store: Store<AppState>,
@Inject(MAT_DIALOG_DATA) public data: AddUserDialogData,
public dialogRef: MatDialogRef<AddUserDialogComponent, User>,
private userService: UserService,
private dialog: MatDialog) {
super(store);
}
ngOnInit(): void {
this.user = {} as User;
this.userComponent.isEdit = true;
this.userComponent.entity = this.user;
this.detailsForm = this.userComponent.entityNgForm;
}
cancel(): void {
this.dialogRef.close(null);
}
add(): void {
if (this.detailsForm.valid) {
this.user = {...this.user, ...this.userComponent.entityForm.value};
this.user.authority = this.data.authority;
this.user.tenantId = new TenantId(this.data.tenantId);
this.user.customerId = new CustomerId(this.data.customerId);
const sendActivationEmail = this.activationMethod === ActivationMethod.SEND_ACTIVATION_MAIL;
this.userService.saveUser(this.user, sendActivationEmail).subscribe(
(user) => {
if (this.activationMethod === ActivationMethod.DISPLAY_ACTIVATION_LINK) {
this.userService.getActivationLink(user.id.id).subscribe(
(activationLink) => {
this.displayActivationLink(activationLink).subscribe(
() => {
this.dialogRef.close(user);
}
);
}
);
} else {
this.dialogRef.close(user);
}
}
);
}
}
displayActivationLink(activationLink: string): Observable<void> {
return this.dialog.open<ActivationLinkDialogComponent, ActivationLinkDialogData,
void>(ActivationLinkDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
activationLink
}
}).afterClosed();
}
}

28
ui-ngx/src/app/modules/home/pages/user/user-routing.module.ts

@ -0,0 +1,28 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { UsersTableConfigResolver } from '@modules/home/pages/user/users-table-config.resolver';
@NgModule({
imports: [],
exports: [RouterModule],
providers: [
UsersTableConfigResolver
]
})
export class UserRoutingModule { }

90
ui-ngx/src/app/modules/home/pages/user/user.component.html

@ -0,0 +1,90 @@
<!--
Copyright © 2016-2019 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="tb-details-buttons">
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'displayActivationLink')"
[fxShow]="!isEdit">
{{'user.display-activation-link' | translate }}
</button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'resendActivation')"
[fxShow]="!isEdit">
{{'user.resend-activation' | translate }}
</button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'loginAsUser')"
*ngIf="loginAsUserEnabled$ | async"
[fxShow]="!isEdit">
{{ (entity?.authority === authority.TENANT_ADMIN ? 'user.login-as-tenant-admin' : 'user.login-as-customer-user') | translate }}
</button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'delete')"
[fxShow]="!hideDelete() && !isEdit">
{{'user.delete' | translate }}
</button>
</div>
<div class="mat-padding" fxLayout="column">
<form #entityNgForm="ngForm" [formGroup]="entityForm">
<fieldset [disabled]="(isLoading$ | async) || !isEdit">
<mat-form-field class="mat-block">
<mat-label translate>user.email</mat-label>
<input matInput formControlName="email" required>
<mat-error *ngIf="entityForm.get('email').hasError('email')">
{{ 'user.invalid-email-format' | translate }}
</mat-error>
<mat-error *ngIf="entityForm.get('email').hasError('required')">
{{ 'user.email-required' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>user.first-name</mat-label>
<input matInput formControlName="firstName">
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>user.last-name</mat-label>
<input matInput formControlName="lastName">
</mat-form-field>
<div formGroupName="additionalInfo" fxLayout="column">
<mat-form-field class="mat-block">
<mat-label translate>user.description</mat-label>
<textarea matInput formControlName="description" rows="2"></textarea>
</mat-form-field>
<section class="tb-default-dashboard" fxFlex fxLayout="column" *ngIf="entity?.id">
<section fxFlex fxLayout="column" fxLayout.gt-sm="row">
<tb-dashboard-autocomplete
fxFlex
placeholder="{{ 'user.default-dashboard' | translate }}"
formControlName="defaultDashboardId"
[dashboardsScope]="entity?.authority === authority.TENANT_ADMIN ? 'tenant' : 'customer'"
[tenantId]="entity?.tenantId?.id"
[customerId]="entity?.customerId?.id"
[selectFirstDashboard]="false"
></tb-dashboard-autocomplete>
<mat-checkbox fxFlex formControlName="defaultDashboardFullscreen">
{{ 'user.always-fullscreen' | translate }}
</mat-checkbox>
</section>
</section>
</div>
</fieldset>
</form>
</div>

35
ui-ngx/src/app/modules/home/pages/user/user.component.scss

@ -0,0 +1,35 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@import "../../../../../scss/constants";
:host {
.tb-default-dashboard {
tb-dashboard-autocomplete {
@media #{$mat-gt-sm} {
padding-right: 12px;
}
@media #{$mat-lt-md} {
padding-bottom: 12px;
}
}
mat-checkbox {
@media #{$mat-gt-sm} {
margin-top: 16px;
}
}
}
}

82
ui-ngx/src/app/modules/home/pages/user/user.component.ts

@ -0,0 +1,82 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, OnInit } from '@angular/core';
import { select, Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { EntityComponent } from '@shared/components/entity/entity.component';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { User } from '@shared/models/user.model';
import { selectAuth, selectUserDetails } from '@core/auth/auth.selectors';
import { map } from 'rxjs/operators';
import { Authority } from '@shared/models/authority.enum';
@Component({
selector: 'tb-user',
templateUrl: './user.component.html',
styleUrls: ['./user.component.scss']
})
export class UserComponent extends EntityComponent<User> {
authority = Authority;
loginAsUserEnabled$ = this.store.pipe(
select(selectAuth),
map((auth) => auth.userTokenAccessEnabled)
);
constructor(protected store: Store<AppState>,
public fb: FormBuilder) {
super(store);
}
hideDelete() {
if (this.entitiesTableConfig) {
return !this.entitiesTableConfig.deleteEnabled(this.entity);
} else {
return false;
}
}
buildForm(entity: User): FormGroup {
return this.fb.group(
{
email: [entity ? entity.email : '', [Validators.required, Validators.email]],
firstName: [entity ? entity.firstName : ''],
lastName: [entity ? entity.lastName : ''],
additionalInfo: this.fb.group(
{
description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''],
defaultDashboardId: [entity && entity.additionalInfo ? entity.additionalInfo.defaultDashboardId : null],
defaultDashboardFullscreen: [entity && entity.additionalInfo ? entity.additionalInfo.defaultDashboardFullscreen : false],
}
)
}
);
}
updateForm(entity: User) {
this.entityForm.patchValue({email: entity.email});
this.entityForm.patchValue({firstName: entity.firstName});
this.entityForm.patchValue({lastName: entity.lastName});
this.entityForm.patchValue({additionalInfo: {description: entity.additionalInfo ? entity.additionalInfo.description : ''}});
this.entityForm.patchValue({additionalInfo:
{defaultDashboardId: entity.additionalInfo ? entity.additionalInfo.defaultDashboardId : null}});
this.entityForm.patchValue({additionalInfo:
{defaultDashboardFullscreen: entity.additionalInfo ? entity.additionalInfo.defaultDashboardFullscreen : false}});
}
}

42
ui-ngx/src/app/modules/home/pages/user/user.module.ts

@ -0,0 +1,42 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SharedModule } from '@shared/shared.module';
import { UserComponent } from '@modules/home/pages/user/user.component';
import { UserRoutingModule } from '@modules/home/pages/user/user-routing.module';
import { AddUserDialogComponent } from '@modules/home/pages/user/add-user-dialog.component';
import { ActivationLinkDialogComponent } from '@modules/home/pages/user/activation-link-dialog.component';
@NgModule({
entryComponents: [
UserComponent,
AddUserDialogComponent,
ActivationLinkDialogComponent
],
declarations: [
UserComponent,
AddUserDialogComponent,
ActivationLinkDialogComponent
],
imports: [
CommonModule,
SharedModule,
UserRoutingModule
]
})
export class UserModule { }

230
ui-ngx/src/app/modules/home/pages/user/users-table-config.resolver.ts

@ -0,0 +1,230 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve } from '@angular/router';
import {
DateEntityTableColumn,
EntityTableColumn,
EntityTableConfig
} from '@shared/components/entity/entities-table-config.models';
import { TranslateService } from '@ngx-translate/core';
import { DatePipe } from '@angular/common';
import {
EntityType,
entityTypeResources,
entityTypeTranslations
} from '@shared/models/entity-type.models';
import { User } from '@shared/models/user.model';
import { UserService } from '@core/http/user.service';
import { UserComponent } from '@modules/home/pages/user/user.component';
import { CustomerService } from '@core/http/customer.service';
import { map, mergeMap, take, tap } from 'rxjs/operators';
import { forkJoin, noop, Observable, of } from 'rxjs';
import { Authority } from '@shared/models/authority.enum';
import { CustomerId } from '@shared/models/id/customer-id';
import { MatDialog } from '@angular/material';
import { EntityAction } from '@shared/components/entity/entity-component.models';
import {
AddUserDialogComponent,
AddUserDialogData
} from '@modules/home/pages/user/add-user-dialog.component';
import { AuthState } from '@core/auth/auth.models';
import { select, Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { selectAuth } from '@core/auth/auth.selectors';
import { AuthService } from '@core/auth/auth.service';
import {
ActivationLinkDialogComponent,
ActivationLinkDialogData
} from '@modules/home/pages/user/activation-link-dialog.component';
import { ActionNotificationShow } from '@core/notification/notification.actions';
import { NULL_UUID } from '@shared/models/id/has-uuid';
import { Customer } from '@shared/models/customer.model';
import {TenantService} from '@app/core/http/tenant.service';
import {TenantId} from '@app/shared/models/id/tenant-id';
export interface UsersTableRouteData {
authority: Authority;
}
@Injectable()
export class UsersTableConfigResolver implements Resolve<EntityTableConfig<User>> {
private readonly config: EntityTableConfig<User> = new EntityTableConfig<User>();
private tenantId: string;
private customerId: string;
private authority: Authority;
private authUser: User;
constructor(private store: Store<AppState>,
private userService: UserService,
private authService: AuthService,
private tenantService: TenantService,
private customerService: CustomerService,
private translate: TranslateService,
private datePipe: DatePipe,
private dialog: MatDialog) {
this.config.entityType = EntityType.USER;
this.config.entityComponent = UserComponent;
this.config.entityTranslations = entityTypeTranslations.get(EntityType.USER);
this.config.entityResources = entityTypeResources.get(EntityType.USER);
this.config.columns.push(
new DateEntityTableColumn<User>('createdTime', 'user.created-time', this.datePipe, '150px'),
new EntityTableColumn<User>('firstName', 'user.first-name'),
new EntityTableColumn<User>('lastName', 'user.last-name'),
new EntityTableColumn<User>('email', 'user.email')
);
this.config.deleteEnabled = user => user && user.id && user.id.id !== this.authUser.id.id;
this.config.deleteEntityTitle = user => this.translate.instant('user.delete-user-title', { userEmail: user.email });
this.config.deleteEntityContent = () => this.translate.instant('user.delete-user-text');
this.config.deleteEntitiesTitle = count => this.translate.instant('user.delete-users-title', {count});
this.config.deleteEntitiesContent = () => this.translate.instant('user.delete-users-text');
this.config.loadEntity = id => this.userService.getUser(id.id);
this.config.saveEntity = user => this.saveUser(user);
this.config.deleteEntity = id => this.userService.deleteUser(id.id);
this.config.onEntityAction = action => this.onUserAction(action);
this.config.addEntity = () => this.addUser();
}
resolve(route: ActivatedRouteSnapshot): Observable<EntityTableConfig<User>> {
const routeParams = route.params;
return this.store.pipe(select(selectAuth), take(1)).pipe(
tap((auth) => {
this.authUser = auth.userDetails;
this.authority = routeParams.tenantId ? Authority.TENANT_ADMIN : Authority.CUSTOMER_USER;
if (this.authority === Authority.TENANT_ADMIN) {
this.tenantId = routeParams.tenantId;
this.customerId = NULL_UUID;
this.config.entitiesFetchFunction = pageLink => this.userService.getTenantAdmins(this.tenantId, pageLink);
} else {
this.tenantId = this.authUser.tenantId.id;
this.customerId = routeParams.customerId;
this.config.entitiesFetchFunction = pageLink => this.userService.getCustomerUsers(this.customerId, pageLink);
}
this.updateActionCellDescriptors(auth);
}),
mergeMap(() => this.authority === Authority.TENANT_ADMIN ?
this.tenantService.getTenant(this.tenantId) :
this.customerService.getCustomer(this.customerId)),
map((parentEntity) => {
if (this.authority === Authority.TENANT_ADMIN) {
this.config.tableTitle = parentEntity.title + ': ' + this.translate.instant('user.tenant-admins');
} else {
this.config.tableTitle = parentEntity.title + ': ' + this.translate.instant('user.customer-users');
}
return this.config;
})
);
}
updateActionCellDescriptors(auth: AuthState) {
this.config.cellActionDescriptors.splice(0);
if (auth.userTokenAccessEnabled) {
this.config.cellActionDescriptors.push(
{
name: this.authority === Authority.TENANT_ADMIN ?
this.translate.instant('user.login-as-tenant-admin') :
this.translate.instant('user.login-as-customer-user'),
icon: 'mdi:login',
isMdiIcon: true,
isEnabled: () => true,
onAction: ($event, entity) => this.loginAsUser($event, entity)
}
);
}
}
saveUser(user: User): Observable<User> {
user.tenantId = new TenantId(this.tenantId);
user.customerId = new CustomerId(this.customerId);
user.authority = this.authority;
return this.userService.saveUser(user);
}
addUser(): Observable<User> {
return this.dialog.open<AddUserDialogComponent, AddUserDialogData,
User>(AddUserDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
tenantId: this.tenantId,
customerId: this.customerId,
authority: this.authority
}
}).afterClosed();
}
loginAsUser($event: Event, user: User) {
if ($event) {
$event.stopPropagation();
}
this.authService.loginAsUser(user.id.id).subscribe();
}
displayActivationLink($event: Event, user: User) {
if ($event) {
$event.stopPropagation();
}
this.userService.getActivationLink(user.id.id).subscribe(
(activationLink) => {
this.dialog.open<ActivationLinkDialogComponent, ActivationLinkDialogData,
void>(ActivationLinkDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
activationLink
}
});
}
);
}
resendActivation($event: Event, user: User) {
if ($event) {
$event.stopPropagation();
}
this.userService.sendActivationEmail(user.email).subscribe(() => {
this.store.dispatch(new ActionNotificationShow(
{
message: this.translate.instant('user.activation-email-sent-message'),
type: 'success'
}));
});
}
onUserAction(action: EntityAction<User>): boolean {
switch (action.action) {
case 'loginAsUser':
this.loginAsUser(action.event, action.entity);
return true;
case 'displayActivationLink':
this.displayActivationLink(action.event, action.entity);
return true;
case 'resendActivation':
this.resendActivation(action.event, action.entity);
return true;
}
return false;
}
}

63
ui-ngx/src/app/shared/components/contact.component.html

@ -0,0 +1,63 @@
<!--
Copyright © 2016-2019 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<section [formGroup]="parentForm">
<mat-form-field class="mat-block">
<mat-label translate>contact.country</mat-label>
<mat-select matInput formControlName="country">
<mat-option *ngFor="let country of countries" [value]="country">
{{ country }}
</mat-option>
</mat-select>
</mat-form-field>
<div fxLayout.gt-sm="row" fxLayoutGap.gt-sm="10px">
<mat-form-field class="mat-block">
<mat-label translate>contact.city</mat-label>
<input matInput formControlName="city">
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>contact.state</mat-label>
<input matInput formControlName="state">
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>contact.postal-code</mat-label>
<input matInput formControlName="zip">
<mat-error *ngIf="parentForm.get('zip').hasError('pattern')">
{{ 'contact.postal-code-invalid' | translate }}
</mat-error>
</mat-form-field>
</div>
<mat-form-field class="mat-block">
<mat-label translate>contact.address</mat-label>
<input matInput formControlName="address">
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>contact.address2</mat-label>
<input matInput formControlName="address2">
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>contact.phone</mat-label>
<input matInput formControlName="phone">
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>contact.email</mat-label>
<input matInput formControlName="email">
<mat-error *ngIf="parentForm.get('email').hasError('email')">
{{ 'user.invalid-email-format' | translate }}
</mat-error>
</mat-form-field>
</section>

34
ui-ngx/src/app/shared/components/contact.component.ts

@ -0,0 +1,34 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, Input } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { COUNTRIES } from '@shared/components/contact.models';
@Component({
selector: 'tb-contact',
templateUrl: './contact.component.html'
})
export class ContactComponent {
@Input()
parentForm: FormGroup;
@Input() isEdit: boolean;
countries = COUNTRIES;
}

291
ui-ngx/src/app/shared/components/contact.models.ts

@ -0,0 +1,291 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
export const COUNTRIES = [
'Afghanistan',
'Åland Islands',
'Albania',
'Algeria',
'American Samoa',
'Andorra',
'Angola',
'Anguilla',
'Antarctica',
'Antigua and Barbuda',
'Argentina',
'Armenia',
'Aruba',
'Australia',
'Austria',
'Azerbaijan',
'Bahamas',
'Bahrain',
'Bangladesh',
'Barbados',
'Belarus',
'Belgium',
'Belize',
'Benin',
'Bermuda',
'Bhutan',
'Bolivia',
'Bonaire, Sint Eustatius and Saba',
'Bosnia and Herzegovina',
'Botswana',
'Bouvet Island',
'Brazil',
'British Indian Ocean Territory',
'Brunei Darussalam',
'Bulgaria',
'Burkina Faso',
'Burundi',
'Cambodia',
'Cameroon',
'Canada',
'Cape Verde',
'Cayman Islands',
'Central African Republic',
'Chad',
'Chile',
'China',
'Christmas Island',
'Cocos (Keeling) Islands',
'Colombia',
'Comoros',
'Congo',
'Congo, The Democratic Republic of the',
'Cook Islands',
'Costa Rica',
'Côte d\'Ivoire',
'Croatia',
'Cuba',
'Curaçao',
'Cyprus',
'Czech Republic',
'Denmark',
'Djibouti',
'Dominica',
'Dominican Republic',
'Ecuador',
'Egypt',
'El Salvador',
'Equatorial Guinea',
'Eritrea',
'Estonia',
'Ethiopia',
'Falkland Islands (Malvinas)',
'Faroe Islands',
'Fiji',
'Finland',
'France',
'French Guiana',
'French Polynesia',
'French Southern Territories',
'Gabon',
'Gambia',
'Georgia',
'Germany',
'Ghana',
'Gibraltar',
'Greece',
'Greenland',
'Grenada',
'Guadeloupe',
'Guam',
'Guatemala',
'Guernsey',
'Guinea',
'Guinea-Bissau',
'Guyana',
'Haiti',
'Heard Island and McDonald Islands',
'Holy See (Vatican City State)',
'Honduras',
'Hong Kong',
'Hungary',
'Iceland',
'India',
'Indonesia',
'Iran, Islamic Republic of',
'Iraq',
'Ireland',
'Isle of Man',
'Israel',
'Italy',
'Jamaica',
'Japan',
'Jersey',
'Jordan',
'Kazakhstan',
'Kenya',
'Kiribati',
'Korea, Democratic People\'s Republic of',
'Korea, Republic of',
'Kuwait',
'Kyrgyzstan',
'Lao People\'s Democratic Republic',
'Latvia',
'Lebanon',
'Lesotho',
'Liberia',
'Libya',
'Liechtenstein',
'Lithuania',
'Luxembourg',
'Macao',
'Macedonia, Republic Of',
'Madagascar',
'Malawi',
'Malaysia',
'Maldives',
'Mali',
'Malta',
'Marshall Islands',
'Martinique',
'Mauritania',
'Mauritius',
'Mayotte',
'Mexico',
'Micronesia, Federated States of',
'Moldova, Republic of',
'Monaco',
'Mongolia',
'Montenegro',
'Montserrat',
'Morocco',
'Mozambique',
'Myanmar',
'Namibia',
'Nauru',
'Nepal',
'Netherlands',
'New Caledonia',
'New Zealand',
'Nicaragua',
'Niger',
'Nigeria',
'Niue',
'Norfolk Island',
'Northern Mariana Islands',
'Norway',
'Oman',
'Pakistan',
'Palau',
'Palestinian Territory, Occupied',
'Panama',
'Papua New Guinea',
'Paraguay',
'Peru',
'Philippines',
'Pitcairn',
'Poland',
'Portugal',
'Puerto Rico',
'Qatar',
'Reunion',
'Romania',
'Russian Federation',
'Rwanda',
'Saint Barthélemy',
'Saint Helena, Ascension and Tristan da Cunha',
'Saint Kitts and Nevis',
'Saint Lucia',
'Saint Martin (French Part)',
'Saint Pierre and Miquelon',
'Saint Vincent and the Grenadines',
'Samoa',
'San Marino',
'Sao Tome and Principe',
'Saudi Arabia',
'Senegal',
'Serbia',
'Seychelles',
'Sierra Leone',
'Singapore',
'Sint Maarten (Dutch Part)',
'Slovakia',
'Slovenia',
'Solomon Islands',
'Somalia',
'South Africa',
'South Georgia and the South Sandwich Islands',
'South Sudan',
'Spain',
'Sri Lanka',
'Sudan',
'Suriname',
'Svalbard and Jan Mayen',
'Swaziland',
'Sweden',
'Switzerland',
'Syrian Arab Republic',
'Taiwan',
'Tajikistan',
'Tanzania, United Republic of',
'Thailand',
'Timor-Leste',
'Togo',
'Tokelau',
'Tonga',
'Trinidad and Tobago',
'Tunisia',
'Turkey',
'Turkmenistan',
'Turks and Caicos Islands',
'Tuvalu',
'Uganda',
'Ukraine',
'United Arab Emirates',
'United Kingdom',
'United States',
'United States Minor Outlying Islands',
'Uruguay',
'Uzbekistan',
'Vanuatu',
'Venezuela',
'Viet Nam',
'Virgin Islands, British',
'Virgin Islands, U.S.',
'Wallis and Futuna',
'Western Sahara',
'Yemen',
'Zambia',
'Zimbabwe'
];
/* tslint:disable */
export const POSTAL_CODE_PATTERNS = {
'United States': '(\\d{5}([\\-]\\d{4})?)',
'Australia': '[0-9]{4}',
'Austria': '[0-9]{4}',
'Belgium': '[0-9]{4}',
'Brazil': '[0-9]{5}[\\-]?[0-9]{3}',
'Canada': '^(?!.*[DFIOQU])[A-VXY][0-9][A-Z][ -]?[0-9][A-Z][0-9]$',
'Denmark': '[0-9]{3,4}',
'Faroe Islands': '[0-9]{3,4}',
'Netherlands': '[1-9][0-9]{3}\\s?[a-zA-Z]{2}',
'Germany': '[0-9]{5}',
'Hungary': '[0-9]{4}',
'Italy': '[0-9]{5}',
'Japan': '\\d{3}-\\d{4}',
'Luxembourg': '(L\\s*(-|—|–))\\s*?[\\d]{4}',
'Poland': '[0-9]{2}\\-[0-9]{3}',
'Spain': '((0[1-9]|5[0-2])|[1-4][0-9])[0-9]{3}',
'Sweden': '\\d{3}\\s?\\d{2}',
'United Kingdom': '[A-Za-z]{1,2}[0-9Rr][0-9A-Za-z]? [0-9][ABD-HJLNP-UW-Zabd-hjlnp-uw-z]{2}'
};
/* tslint:enable */

46
ui-ngx/src/app/shared/components/dashboard-autocomplete.component.html

@ -0,0 +1,46 @@
<!--
Copyright © 2016-2019 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<mat-form-field [formGroup]="selectDashboardFormGroup" class="mat-block">
<input matInput type="text" placeholder="{{ placeholder || ('dashboard.dashboard' | translate) }}"
#dashboardInput
formControlName="dashboard"
[required]="required"
[matAutocomplete]="dashboardAutocomplete">
<button *ngIf="selectDashboardFormGroup.get('dashboard').value && !disabled"
type="button"
matSuffix mat-button mat-icon-button aria-label="Clear"
(click)="clear()">
<mat-icon class="material-icons">close</mat-icon>
</button>
<mat-autocomplete #dashboardAutocomplete="matAutocomplete" [displayWith]="displayDashboardFn">
<mat-option *ngFor="let dashboard of filteredDashboards | async" [value]="dashboard">
<span [innerHTML]="dashboard.title | highlight:searchText"></span>
</mat-option>
<mat-option *ngIf="!(filteredDashboards | async)?.length" [value]="null">
<span>
{{ translate.get('dashboard.no-dashboards-matching', {entity: searchText}) | async }}
</span>
</mat-option>
</mat-autocomplete>
<mat-error>
<ng-content select="[tb-error]"></ng-content>
</mat-error>
<mat-hint>
<ng-content select="[tb-hint]"></ng-content>
</mat-hint>
</mat-form-field>

226
ui-ngx/src/app/shared/components/dashboard-autocomplete.component.ts

@ -0,0 +1,226 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {AfterViewInit, Component, ElementRef, forwardRef, Input, OnInit, ViewChild} from '@angular/core';
import {ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR} from '@angular/forms';
import {Observable, of} from 'rxjs';
import {PageLink} from '@shared/models/page/page-link';
import {Direction} from '@shared/models/page/sort-order';
import {map, mergeMap, startWith, tap} from 'rxjs/operators';
import {PageData, emptyPageData} from '@shared/models/page/page-data';
import {DashboardInfo} from '@app/shared/models/dashboard.models';
import {DashboardId} from '@app/shared/models/id/dashboard-id';
import {DashboardService} from '@core/http/dashboard.service';
import {Store} from '@ngrx/store';
import {AppState} from '@app/core/core.state';
import {getCurrentAuthUser} from '@app/core/auth/auth.selectors';
import {Authority} from '@shared/models/authority.enum';
import {TranslateService} from '@ngx-translate/core';
@Component({
selector: 'tb-dashboard-autocomplete',
templateUrl: './dashboard-autocomplete.component.html',
styleUrls: [],
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DashboardAutocompleteComponent),
multi: true
}]
})
export class DashboardAutocompleteComponent implements ControlValueAccessor, OnInit, AfterViewInit {
selectDashboardFormGroup: FormGroup;
modelValue: DashboardInfo | string | null;
@Input()
useIdValue = true;
@Input()
selectFirstDashboard = false;
@Input()
placeholder: string;
@Input()
dashboardsScope: 'customer' | 'tenant';
@Input()
tenantId: string;
@Input()
customerId: string;
@Input()
required: boolean;
@Input()
disabled: boolean;
@ViewChild('dashboardInput', {static: true}) dashboardInput: ElementRef;
filteredDashboards: Observable<Array<DashboardInfo>>;
private valueLoaded = false;
private searchText = '';
private propagateChange = (v: any) => { };
constructor(private store: Store<AppState>,
public translate: TranslateService,
private dashboardService: DashboardService,
private fb: FormBuilder) {
this.selectDashboardFormGroup = this.fb.group({
dashboard: [null]
});
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
ngOnInit() {
}
ngAfterViewInit(): void {
this.selectFirstDashboardIfNeeded();
}
selectFirstDashboardIfNeeded(): void {
if (this.selectFirstDashboard && !this.modelValue) {
this.getDashboards(new PageLink(1)).subscribe(
(data) => {
if (data.data.length) {
const dashboard = data.data[0];
this.modelValue = this.useIdValue ? dashboard.id.id : dashboard;
this.selectDashboardFormGroup.get('dashboard').patchValue(dashboard, {emitEvent: false});
this.propagateChange(this.modelValue);
}
}
);
}
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}
initFilteredResults(): void {
this.filteredDashboards = this.selectDashboardFormGroup.get('dashboard').valueChanges
.pipe(
startWith<string | DashboardInfo>(''),
tap(value => {
if (this.valueLoaded) {
let modelValue;
if (typeof value === 'string' || !value) {
modelValue = null;
} else {
modelValue = this.useIdValue ? value.id.id : value;
}
this.updateView(modelValue);
}
}),
map(value => value ? (typeof value === 'string' ? value : value.name) : ''),
mergeMap(name => this.fetchDashboards(name) )
);
}
writeValue(value: DashboardInfo | string | null): void {
this.valueLoaded = false;
this.searchText = '';
this.initFilteredResults();
if (value != null) {
if (typeof value === 'string') {
this.dashboardService.getDashboardInfo(value).subscribe(
(dashboard) => {
this.modelValue = this.useIdValue ? dashboard.id.id : dashboard;
this.selectDashboardFormGroup.get('dashboard').patchValue(dashboard, {emitEvent: true});
this.valueLoaded = true;
}
);
} else {
this.modelValue = this.useIdValue ? value.id.id : value;
this.selectDashboardFormGroup.get('dashboard').patchValue(value, {emitEvent: false});
this.valueLoaded = true;
}
} else {
this.modelValue = null;
this.selectDashboardFormGroup.get('dashboard').patchValue(null, {emitEvent: false});
this.valueLoaded = true;
}
}
updateView(value: DashboardInfo | string | null) {
if (this.modelValue !== value) {
this.modelValue = value;
this.propagateChange(this.modelValue);
}
}
displayDashboardFn(dashboard?: DashboardInfo): string | undefined {
return dashboard ? dashboard.title : undefined;
}
fetchDashboards(searchText?: string): Observable<Array<DashboardInfo>> {
this.searchText = searchText;
const pageLink = new PageLink(10, 0, searchText, {
property: 'title',
direction: Direction.ASC
});
return this.getDashboards(pageLink).pipe(
map(pageData => {
return pageData.data;
})
);
}
getDashboards(pageLink: PageLink): Observable<PageData<DashboardInfo>> {
let dashboardsObservable: Observable<PageData<DashboardInfo>>;
const authUser = getCurrentAuthUser(this.store);
if (this.dashboardsScope === 'customer' || authUser.authority === Authority.CUSTOMER_USER) {
if (this.customerId) {
dashboardsObservable = this.dashboardService.getCustomerDashboards(this.customerId, pageLink, false, true);
} else {
dashboardsObservable = of(emptyPageData());
}
} else {
if (authUser.authority === Authority.SYS_ADMIN) {
if (this.tenantId) {
dashboardsObservable = this.dashboardService.getTenantDashboardsByTenantId(this.tenantId, pageLink, false, true);
} else {
dashboardsObservable = of(emptyPageData());
}
} else {
dashboardsObservable = this.dashboardService.getTenantDashboards(pageLink, false, true);
}
}
return dashboardsObservable;
}
clear() {
this.selectDashboardFormGroup.get('dashboard').patchValue(null, {emitEvent: true});
setTimeout(() => {
this.dashboardInput.nativeElement.blur();
this.dashboardInput.nativeElement.focus();
}, 0);
}
}

56
ui-ngx/src/app/shared/components/details-panel.component.html

@ -0,0 +1,56 @@
<!--
Copyright © 2016-2019 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<header>
<mat-toolbar color="primary" [ngStyle]="{height: headerHeightPx+'px'}">
<div fxFlex fxLayout="row" fxLayoutAlign="start center">
<div class="mat-toolbar-tools" fxFlex fxLayout="column" fxLayoutAlign="start start">
<span class="tb-details-title">{{ headerTitle }}</span>
<span class="tb-details-subtitle">{{ headerSubtitle }}</span>
<span style="width: 100%;">
<ng-content select=".header-pane"></ng-content>
</span>
</div>
<ng-content select=".details-buttons"></ng-content>
<button mat-button mat-icon-button (click)="onCloseDetails()">
<mat-icon class="material-icons">close</mat-icon>
</button>
</div>
<section *ngIf="!isReadOnly" fxLayout="row" class="layout-wrap tb-header-buttons">
<button [disabled]="(isLoading$ | async) || theForm.invalid || !theForm.dirty"
mat-fab
matTooltip="{{ 'action.apply-changes' | translate }}"
matTooltipPosition="above"
color="accent" class="tb-btn-header mat-fab-bottom-right"
[ngClass]="{'tb-hide': !isEdit}"
(click)="onApplyDetails()">
<mat-icon class="material-icons">done</mat-icon>
</button>
<button [disabled]="(isLoading$ | async) || (isAlwaysEdit && !theForm.dirty)"
mat-fab
matTooltip="{{ (isAlwaysEdit ? 'action.decline-changes' : 'details.toggle-edit-mode') | translate }}"
matTooltipPosition="above"
color="accent" class="tb-btn-header mat-fab-bottom-right"
(click)="onToggleDetailsEditMode()">
<mat-icon class="material-icons">{{isEdit ? 'close' : 'edit'}}</mat-icon>
</button>
</section>
</mat-toolbar>
</header>
<div fxFlex class="mat-content">
<ng-content></ng-content>
</div>

53
ui-ngx/src/app/shared/components/details-panel.component.scss

@ -0,0 +1,53 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@import '../../../scss/constants';
:host {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
.mat-toolbar-tools {
height: 100%;
min-height: 100px;
max-height: 120px;
}
.tb-details-title {
width: inherit;
margin: 20px 8px 0 0;
overflow: hidden;
font-size: 1rem;
font-weight: 400;
text-overflow: ellipsis;
text-transform: uppercase;
white-space: nowrap;
@media #{$mat-gt-sm} {
font-size: 1.6rem;
}
}
.tb-details-subtitle {
width: inherit;
margin: 10px 0;
overflow: hidden;
font-size: 1rem;
text-overflow: ellipsis;
white-space: nowrap;
opacity: .8;
}
}

80
ui-ngx/src/app/shared/components/details-panel.component.ts

@ -0,0 +1,80 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { NgForm } from '@angular/forms';
@Component({
selector: 'tb-details-panel',
templateUrl: './details-panel.component.html',
styleUrls: ['./details-panel.component.scss']
})
export class DetailsPanelComponent extends PageComponent {
@Input() headerHeightPx = 100;
@Input() headerTitle = '';
@Input() headerSubtitle = '';
@Input() isReadOnly = false;
@Input() isAlwaysEdit = false;
@Input() theForm: NgForm;
@Output()
closeDetails = new EventEmitter<void>();
@Output()
toggleDetailsEditMode = new EventEmitter<boolean>();
@Output()
applyDetails = new EventEmitter<void>();
isEditValue = false;
@Output()
isEditChange = new EventEmitter<boolean>();
@Input()
get isEdit() {
return this.isEditValue;
}
set isEdit(val: boolean) {
this.isEditValue = val;
this.isEditChange.emit(this.isEditValue);
}
constructor(protected store: Store<AppState>) {
super(store);
}
onCloseDetails() {
this.closeDetails.emit();
}
onToggleDetailsEditMode() {
if (!this.isAlwaysEdit) {
this.isEdit = !this.isEdit;
}
this.toggleDetailsEditMode.emit(this.isEditValue);
}
onApplyDetails() {
if (this.theForm.valid) {
this.applyDetails.emit();
}
}
}

51
ui-ngx/src/app/shared/components/entity/add-entity-dialog.component.html

@ -0,0 +1,51 @@
<!--
Copyright © 2016-2019 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<form (ngSubmit)="add()" style="min-width: 400px;">
<mat-toolbar fxLayout="row" color="primary">
<h2 translate>{{ translations.add }}</h2>
<span fxFlex></span>
<div [tb-help]="resources.helpLinkId"></div>
<button mat-button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div mat-dialog-content>
<tb-anchor #entityDetailsForm></tb-anchor>
</div>
<div mat-dialog-actions fxLayout="row">
<span fxFlex></span>
<button mat-button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || detailsForm.invalid || !detailsForm.dirty">
{{ 'action.add' | translate }}
</button>
<button mat-button color="primary"
style="margin-right: 20px;"
type="button"
cdkFocusInitial
[disabled]="(isLoading$ | async)"
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
</div>
</form>

17
ui-ngx/src/app/shared/components/entity/add-entity-dialog.component.scss

@ -0,0 +1,17 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
}

103
ui-ngx/src/app/shared/components/entity/add-entity-dialog.component.ts

@ -0,0 +1,103 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
Component,
ComponentFactoryResolver,
Inject,
OnInit,
SkipSelf,
ViewChild
} from '@angular/core';
import { ErrorStateMatcher, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormControl, FormGroupDirective, NgForm } from '@angular/forms';
import { EntityTypeResource, EntityTypeTranslation } from '@shared/models/entity-type.models';
import { EntityTableConfig } from '@shared/components/entity/entities-table-config.models';
import { BaseData, HasId } from '@shared/models/base-data';
import { EntityId } from '@shared/models/id/entity-id';
import { AddEntityDialogData } from '@shared/components/entity/entity-component.models';
import { TbAnchorComponent } from '@shared/components/tb-anchor.component';
import { EntityComponent } from '@shared/components/entity/entity.component';
@Component({
selector: 'tb-add-entity-dialog',
templateUrl: './add-entity-dialog.component.html',
providers: [{provide: ErrorStateMatcher, useExisting: AddEntityDialogComponent}],
styleUrls: ['./add-entity-dialog.component.scss']
})
export class AddEntityDialogComponent extends PageComponent implements OnInit, ErrorStateMatcher {
entityComponent: EntityComponent<BaseData<HasId>>;
detailsForm: NgForm;
entitiesTableConfig: EntityTableConfig<BaseData<HasId>>;
translations: EntityTypeTranslation;
resources: EntityTypeResource;
entity: BaseData<EntityId>;
submitted = false;
@ViewChild('entityDetailsForm', {static: true}) entityDetailsFormAnchor: TbAnchorComponent;
constructor(protected store: Store<AppState>,
@Inject(MAT_DIALOG_DATA) public data: AddEntityDialogData<BaseData<HasId>>,
public dialogRef: MatDialogRef<AddEntityDialogComponent, BaseData<HasId>>,
private componentFactoryResolver: ComponentFactoryResolver,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher) {
super(store);
}
ngOnInit(): void {
this.entitiesTableConfig = this.data.entitiesTableConfig;
this.translations = this.entitiesTableConfig.entityTranslations;
this.resources = this.entitiesTableConfig.entityResources;
this.entity = {};
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.entitiesTableConfig.entityComponent);
const viewContainerRef = this.entityDetailsFormAnchor.viewContainerRef;
viewContainerRef.clear();
const componentRef = viewContainerRef.createComponent(componentFactory);
this.entityComponent = componentRef.instance;
this.entityComponent.isEdit = true;
this.entityComponent.entitiesTableConfig = this.entitiesTableConfig;
this.entityComponent.entity = this.entity;
this.detailsForm = this.entityComponent.entityNgForm;
}
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
const customErrorState = !!(control && control.invalid && this.submitted);
return originalErrorState || customErrorState;
}
cancel(): void {
this.dialogRef.close(null);
}
add(): void {
this.submitted = true;
if (this.detailsForm.valid) {
this.entity = {...this.entity, ...this.entityComponent.entityFormValue()};
this.entitiesTableConfig.saveEntity(this.entity).subscribe(
(entity) => {
this.dialogRef.close(entity);
}
);
}
}
}

84
ui-ngx/src/app/shared/components/entity/contact-based.component.ts

@ -0,0 +1,84 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { EntityComponent } from '@shared/components/entity/entity.component';
import { FormBuilder, FormGroup, ValidatorFn, Validators } from '@angular/forms';
import { ContactBased } from '@shared/models/contact-based.model';
import { AfterViewInit } from '@angular/core';
import { POSTAL_CODE_PATTERNS } from '@shared/components/contact.models';
import { HasId } from '@shared/models/base-data';
export abstract class ContactBasedComponent<T extends ContactBased<HasId>> extends EntityComponent<T> implements AfterViewInit {
constructor(protected store: Store<AppState>,
protected fb: FormBuilder) {
super(store);
}
buildForm(entity: T): FormGroup {
const entityForm = this.buildEntityForm(entity);
entityForm.addControl('country', this.fb.control(entity ? entity.country : '', []));
entityForm.addControl('city', this.fb.control(entity ? entity.city : '', []));
entityForm.addControl('state', this.fb.control(entity ? entity.state : '', []));
entityForm.addControl('zip', this.fb.control(entity ? entity.zip : '',
this.zipValidators(entity ? entity.country : '')
));
entityForm.addControl('address', this.fb.control(entity ? entity.address : '', []));
entityForm.addControl('address2', this.fb.control(entity ? entity.address2 : '', []));
entityForm.addControl('phone', this.fb.control(entity ? entity.phone : '', []));
entityForm.addControl('email', this.fb.control(entity ? entity.email : '', [Validators.email]));
return entityForm;
}
updateForm(entity: T) {
this.updateEntityForm(entity);
this.entityForm.patchValue({country: entity.country});
this.entityForm.patchValue({city: entity.city});
this.entityForm.patchValue({state: entity.state});
this.entityForm.get('zip').setValidators(this.zipValidators(entity.country));
this.entityForm.patchValue({zip: entity.zip});
this.entityForm.patchValue({address: entity.address});
this.entityForm.patchValue({address2: entity.address2});
this.entityForm.patchValue({phone: entity.phone});
this.entityForm.patchValue({email: entity.email});
}
ngAfterViewInit() {
this.entityForm.get('country').valueChanges.subscribe(
(country) => {
this.entityForm.get('zip').setValidators(this.zipValidators(country));
this.entityForm.get('zip').updateValueAndValidity({onlySelf: true});
this.entityForm.get('zip').markAsTouched({onlySelf: true});
}
);
}
zipValidators(country: string): ValidatorFn[] {
const zipValidators = [];
if (country && POSTAL_CODE_PATTERNS[country]) {
const postalCodePattern = POSTAL_CODE_PATTERNS[country];
zipValidators.push(Validators.pattern(postalCodePattern));
}
return zipValidators;
}
abstract buildEntityForm(entity: T): FormGroup;
abstract updateEntityForm(entity: T);
}

137
ui-ngx/src/app/shared/components/entity/entities-table-config.models.ts

@ -0,0 +1,137 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { BaseData, HasId } from '@shared/models/base-data';
import { EntityId } from '@shared/models/id/entity-id';
import { EntitiesFetchFunction } from '@shared/models/datasource/entity-datasource';
import { Observable, of } from 'rxjs';
import { emptyPageData } from '@shared/models/page/page-data';
import { DatePipe } from '@angular/common';
import { Direction, SortOrder } from '@shared/models/page/sort-order';
import {
EntityType,
EntityTypeResource,
EntityTypeTranslation
} from '@shared/models/entity-type.models';
import { EntityComponent } from '@shared/components/entity/entity.component';
import { Type } from '@angular/core';
import { EntityAction } from '@shared/components/entity/entity-component.models';
import { HasUUID } from '@shared/models/id/has-uuid';
import { PageLink } from '@shared/models/page/page-link';
import { EntitiesTableComponent } from '@shared/components/entity/entities-table.component';
import { EntityTableHeaderComponent } from '@shared/components/entity/entity-table-header.component';
import { ActivatedRoute } from '@angular/router';
export type EntityBooleanFunction<T extends BaseData<HasId>> = (entity: T) => boolean;
export type EntityStringFunction<T extends BaseData<HasId>> = (entity: T) => string;
export type EntityCountStringFunction = (count: number) => string;
export type EntityTwoWayOperation<T extends BaseData<HasId>> = (entity: T) => Observable<T>;
export type EntityByIdOperation<T extends BaseData<HasId>> = (id: HasUUID) => Observable<T>;
export type EntityIdOneWayOperation = (id: HasUUID) => Observable<any>;
export type EntityActionFunction<T extends BaseData<HasId>> = (action: EntityAction<T>) => boolean;
export type CreateEntityOperation<T extends BaseData<HasId>> = () => Observable<T>;
export type CellContentFunction<T extends BaseData<HasId>> = (entity: T, key: string) => string;
export type CellStyleFunction<T extends BaseData<HasId>> = (entity: T, key: string) => object;
export interface CellActionDescriptor<T extends BaseData<HasId>> {
name: string;
nameFunction?: (entity: T) => string;
icon?: string;
isMdiIcon?: boolean;
color?: string;
isEnabled: (entity: T) => boolean;
onAction: ($event: MouseEvent, entity: T) => void;
}
export interface GroupActionDescriptor<T extends BaseData<HasId>> {
name: string;
icon: string;
isEnabled: boolean;
onAction: ($event: MouseEvent, entities: T[]) => void;
}
export interface HeaderActionDescriptor {
name: string;
icon: string;
isEnabled: () => boolean;
onAction: ($event: MouseEvent) => void;
}
export class EntityTableColumn<T extends BaseData<HasId>> {
constructor(public key: string,
public title: string,
public maxWidth: string = '100%',
public cellContentFunction: CellContentFunction<T> = (entity, property) => entity[property],
public cellStyleFunction: CellStyleFunction<T> = () => ({})) {
}
}
export class DateEntityTableColumn<T extends BaseData<HasId>> extends EntityTableColumn<T> {
constructor(key: string,
title: string,
datePipe: DatePipe,
maxWidth: string = '100%',
dateFormat: string = 'yyyy-MM-dd HH:mm:ss',
cellStyleFunction: CellStyleFunction<T> = () => ({})) {
super(key,
title,
maxWidth,
(entity, property) => datePipe.transform(entity[property], dateFormat),
cellStyleFunction);
}
}
export class EntityTableConfig<T extends BaseData<HasId>, P extends PageLink = PageLink> {
constructor() {}
componentsData: any = null;
loadDataOnInit = true;
onLoadAction: (route: ActivatedRoute) => void = null;
table: EntitiesTableComponent = null;
useTimePageLink = false;
entityType: EntityType = null;
tableTitle = '';
selectionEnabled = true;
searchEnabled = true;
addEnabled = true;
entitiesDeleteEnabled = true;
detailsPanelEnabled = true;
actionsColumnTitle = null;
entityTranslations: EntityTypeTranslation;
entityResources: EntityTypeResource;
entityComponent: Type<EntityComponent<T>>;
defaultSortOrder: SortOrder = {property: 'createdTime', direction: Direction.ASC};
columns: Array<EntityTableColumn<T>> = [];
cellActionDescriptors: Array<CellActionDescriptor<T>> = [];
groupActionDescriptors: Array<GroupActionDescriptor<T>> = [];
headerActionDescriptors: Array<HeaderActionDescriptor> = [];
headerComponent: Type<EntityTableHeaderComponent<T>>;
addEntity: CreateEntityOperation<T> = null;
detailsReadonly: EntityBooleanFunction<T> = () => false;
deleteEnabled: EntityBooleanFunction<T> = () => true;
deleteEntityTitle: EntityStringFunction<T> = () => '';
deleteEntityContent: EntityStringFunction<T> = () => '';
deleteEntitiesTitle: EntityCountStringFunction = () => '';
deleteEntitiesContent: EntityCountStringFunction = () => '';
loadEntity: EntityByIdOperation<T> = () => of();
saveEntity: EntityTwoWayOperation<T> = (entity) => of(entity);
deleteEntity: EntityIdOneWayOperation = () => of();
entitiesFetchFunction: EntitiesFetchFunction<T, P> = () => of(emptyPageData<T>());
onEntityAction: EntityActionFunction<T> = () => false;
}

182
ui-ngx/src/app/shared/components/entity/entities-table.component.html

@ -0,0 +1,182 @@
<!--
Copyright © 2016-2019 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<mat-drawer-container hasBackdrop="false" class="tb-absolute-fill">
<mat-drawer *ngIf="entitiesTableConfig.detailsPanelEnabled"
class="tb-details-drawer mat-elevation-z4"
#drawer
mode="over"
position="end"
[opened]="isDetailsOpen">
<tb-entity-details-panel
[entitiesTableConfig]="entitiesTableConfig"
[entityId]="dataSource.currentEntity?.id"
(closeEntityDetails)="isDetailsOpen = false"
(entityUpdated)="onEntityUpdated($event)"
(entityAction)="onEntityAction($event)"
>
</tb-entity-details-panel>
</mat-drawer>
<mat-drawer-content>
<div class="mat-padding tb-entity-table tb-absolute-fill">
<div fxFlex fxLayout="column" class="mat-elevation-z1 tb-entity-table-content">
<mat-toolbar class="mat-table-toolbar" [fxShow]="!textSearchMode && dataSource.selection.isEmpty()">
<div class="mat-toolbar-tools">
<span *ngIf="entitiesTableConfig.tableTitle" class="tb-entity-table-title">{{ entitiesTableConfig.tableTitle }}</span>
<tb-timewindow *ngIf="entitiesTableConfig.useTimePageLink" [(ngModel)]="timewindow"
(ngModelChange)="onTimewindowChange()"
asButton historyOnly></tb-timewindow>
<tb-anchor #entityTableHeader></tb-anchor>
<span fxFlex *ngIf="!this.entitiesTableConfig.headerComponent"></span>
<button mat-button mat-icon-button [disabled]="isLoading$ | async" [fxShow]="addEnabled()" (click)="addEntity($event)"
matTooltip="{{ translations.add | translate }}"
matTooltipPosition="above">
<mat-icon>add</mat-icon>
</button>
<button mat-button mat-icon-button [disabled]="isLoading$ | async"
[fxShow]="actionDescriptor.isEnabled()" *ngFor="let actionDescriptor of headerActionDescriptors"
matTooltip="{{ actionDescriptor.name }}"
matTooltipPosition="above"
(click)="actionDescriptor.onAction($event)">
<mat-icon>{{actionDescriptor.icon}}</mat-icon>
</button>
<button mat-button mat-icon-button [disabled]="isLoading$ | async" (click)="updateData()"
matTooltip="{{ 'action.refresh' | translate }}"
matTooltipPosition="above">
<mat-icon>refresh</mat-icon>
</button>
<button *ngIf="entitiesTableConfig.searchEnabled"
mat-button mat-icon-button [disabled]="isLoading$ | async" (click)="enterFilterMode()"
matTooltip="{{ translations.search | translate }}"
matTooltipPosition="above">
<mat-icon>search</mat-icon>
</button>
</div>
</mat-toolbar>
<mat-toolbar class="mat-table-toolbar" [fxShow]="textSearchMode && dataSource.selection.isEmpty()">
<div class="mat-toolbar-tools">
<button mat-button mat-icon-button
matTooltip="{{ translations.search | translate }}"
matTooltipPosition="above">
<mat-icon>search</mat-icon>
</button>
<mat-form-field fxFlex>
<mat-label>&nbsp;</mat-label>
<input #searchInput matInput
[(ngModel)]="pageLink.textSearch"
placeholder="{{ translations.search | translate }}"/>
</mat-form-field>
<button mat-button mat-icon-button (click)="exitFilterMode()"
matTooltip="{{ 'action.close' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button>
</div>
</mat-toolbar>
<mat-toolbar *ngIf="entitiesTableConfig.selectionEnabled" class="mat-table-toolbar" color="primary" [fxShow]="!dataSource.selection.isEmpty()">
<div class="mat-toolbar-tools">
<span>
{{ translate.get(translations.selectedEntities, {count: dataSource.selection.selected.length}) | async }}
</span>
<span fxFlex></span>
<button mat-button mat-icon-button [disabled]="isLoading$ | async"
[fxShow]="actionDescriptor.isEnabled" *ngFor="let actionDescriptor of groupActionDescriptors"
matTooltip="{{ actionDescriptor.name }}"
matTooltipPosition="above"
(click)="actionDescriptor.onAction($event, dataSource.selection.selected)">
<mat-icon>{{actionDescriptor.icon}}</mat-icon>
</button>
</div>
</mat-toolbar>
<div fxFlex class="table-container">
<mat-table [dataSource]="dataSource"
matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="(pageLink.sortOrder.direction + '').toLowerCase()" matSortDisableClear>
<ng-container matColumnDef="select" sticky>
<mat-header-cell *matHeaderCellDef>
<mat-checkbox (change)="$event ? dataSource.masterToggle() : null"
[checked]="dataSource.selection.hasValue() && (dataSource.isAllSelected() | async)"
[indeterminate]="dataSource.selection.hasValue() && !(dataSource.isAllSelected() | async)">
</mat-checkbox>
</mat-header-cell>
<mat-cell *matCellDef="let entity">
<mat-checkbox (click)="$event.stopPropagation()"
(change)="$event ? dataSource.selection.toggle(entity) : null"
[checked]="dataSource.selection.isSelected(entity)">
</mat-checkbox>
</mat-cell>
</ng-container>
<ng-container [matColumnDef]="column.key" *ngFor="let column of columns">
<mat-header-cell *matHeaderCellDef [ngStyle]="{maxWidth: column.maxWidth}" mat-sort-header> {{ column.title | translate }} </mat-header-cell>
<mat-cell *matCellDef="let entity" [ngStyle]="cellStyle(entity, column)" [innerHTML]="cellContent(entity, column)"></mat-cell>
</ng-container>
<ng-container matColumnDef="actions" stickyEnd>
<mat-header-cell *matHeaderCellDef [ngStyle.gt-md]="{ minWidth: (cellActionDescriptors.length * 40) + 'px' }">
{{ entitiesTableConfig.actionsColumnTitle ? (entitiesTableConfig.actionsColumnTitle | translate) : '' }}
</mat-header-cell>
<mat-cell *matCellDef="let entity" [ngStyle.gt-md]="{ minWidth: (cellActionDescriptors.length * 40) + 'px' }">
<div fxHide fxShow.gt-md fxFlex fxLayout="row" fxLayoutAlign="end">
<button mat-button mat-icon-button [disabled]="isLoading$ | async"
[fxShow]="actionDescriptor.isEnabled(entity)" *ngFor="let actionDescriptor of cellActionDescriptors"
matTooltip="{{ actionDescriptor.nameFunction ? actionDescriptor.nameFunction(entity) : actionDescriptor.name }}"
matTooltipPosition="above"
(click)="actionDescriptor.onAction($event, entity)">
<mat-icon *ngIf="!actionDescriptor.isMdiIcon" [ngStyle]="actionDescriptor.color ? {color: actionDescriptor.color} : {}">
{{actionDescriptor.icon}}</mat-icon>
<mat-icon *ngIf="actionDescriptor.isMdiIcon" [ngStyle]="actionDescriptor.color ? {color: actionDescriptor.color} : {}"
[svgIcon]="actionDescriptor.icon"></mat-icon>
</button>
</div>
<div fxHide fxShow.lt-lg>
<button mat-button mat-icon-button
(click)="$event.stopPropagation()"
[matMenuTriggerFor]="cellActionsMenu">
<mat-icon class="material-icons">more_vert</mat-icon>
</button>
<mat-menu #cellActionsMenu="matMenu" xPosition="before">
<button mat-menu-item *ngFor="let actionDescriptor of cellActionDescriptors"
[disabled]="isLoading$ | async"
[fxShow]="actionDescriptor.isEnabled(entity)"
(click)="actionDescriptor.onAction($event, entity)">
<mat-icon *ngIf="!actionDescriptor.isMdiIcon" [ngStyle]="actionDescriptor.color ? {color: actionDescriptor.color} : {}">
{{actionDescriptor.icon}}</mat-icon>
<mat-icon *ngIf="actionDescriptor.isMdiIcon" [ngStyle]="actionDescriptor.color ? {color: actionDescriptor.color} : {}"
[svgIcon]="actionDescriptor.icon"></mat-icon>
<span>{{ actionDescriptor.nameFunction ? actionDescriptor.nameFunction(entity) : actionDescriptor.name }}</span>
</button>
</mat-menu>
</div>
</mat-cell>
</ng-container>
<mat-header-row [ngClass]="{'mat-row-select': selectionEnabled}" *matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row>
<mat-row [ngClass]="{'mat-row-select': selectionEnabled,
'mat-selected': dataSource.selection.isSelected(entity),
'tb-current-entity': dataSource.isCurrentEntity(entity)}"
*matRowDef="let entity; columns: displayedColumns;" (click)="onRowClick($event, entity)"></mat-row>
</mat-table>
<span [fxShow]="dataSource.isEmpty() | async"
fxLayoutAlign="center center"
class="no-data-found" translate>{{ translations.noEntities }}</span>
</div>
<mat-divider></mat-divider>
<mat-paginator [length]="dataSource.total() | async"
[pageIndex]="pageLink.page"
[pageSize]="pageLink.pageSize"
[pageSizeOptions]="[10, 20, 30]"></mat-paginator>
</div>
</div>
</mat-drawer-content>
</mat-drawer-container>

41
ui-ngx/src/app/shared/components/entity/entities-table.component.scss

@ -0,0 +1,41 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
width: 100%;
height: 100%;
.tb-entity-table {
.tb-entity-table-content {
width: 100%;
height: 100%;
background: #fff;
.tb-entity-table-title {
padding-right: 20px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.table-container {
overflow: auto;
}
}
}
}
:host ::ng-deep .mat-sort-header-sorted .mat-sort-header-arrow {
opacity: 1 !important;
}

358
ui-ngx/src/app/shared/components/entity/entities-table.component.ts

@ -0,0 +1,358 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
AfterViewInit,
Component, ComponentFactoryResolver,
ElementRef,
Input,
OnInit,
Type,
ViewChild
} from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { PageLink, TimePageLink } from '@shared/models/page/page-link';
import { MatDialog, MatPaginator, MatSort } from '@angular/material';
import { EntitiesDataSource } from '@shared/models/datasource/entity-datasource';
import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators';
import { Direction, SortOrder } from '@shared/models/page/sort-order';
import { forkJoin, fromEvent, merge, Observable } from 'rxjs';
import { TranslateService } from '@ngx-translate/core';
import { BaseData, HasId } from '@shared/models/base-data';
import { EntityId } from '@shared/models/id/entity-id';
import { ActivatedRoute } from '@angular/router';
import {
CellActionDescriptor,
EntityTableColumn,
EntityTableConfig,
GroupActionDescriptor,
HeaderActionDescriptor
} from '@shared/components/entity/entities-table-config.models';
import { EntityTypeTranslation } from '@shared/models/entity-type.models';
import { DialogService } from '@core/services/dialog.service';
import { AddEntityDialogComponent } from '@shared/components/entity/add-entity-dialog.component';
import {
AddEntityDialogData,
EntityAction
} from '@shared/components/entity/entity-component.models';
import { Timewindow } from '@shared/models/time/time.models';
import { DomSanitizer } from '@angular/platform-browser';
import { TbAnchorComponent } from '@shared/components/tb-anchor.component';
@Component({
selector: 'tb-entities-table',
templateUrl: './entities-table.component.html',
styleUrls: ['./entities-table.component.scss']
})
export class EntitiesTableComponent extends PageComponent implements AfterViewInit, OnInit {
@Input()
entitiesTableConfig: EntityTableConfig<BaseData<HasId>>;
translations: EntityTypeTranslation;
headerActionDescriptors: Array<HeaderActionDescriptor>;
groupActionDescriptors: Array<GroupActionDescriptor<BaseData<HasId>>>;
cellActionDescriptors: Array<CellActionDescriptor<BaseData<HasId>>>;
columns: Array<EntityTableColumn<BaseData<HasId>>>;
displayedColumns: string[] = [];
selectionEnabled;
pageLink: PageLink;
textSearchMode = false;
timewindow: Timewindow;
dataSource: EntitiesDataSource<BaseData<HasId>>;
isDetailsOpen = false;
@ViewChild('entityTableHeader', {static: false}) entityTableHeaderAnchor: TbAnchorComponent;
@ViewChild('searchInput', {static: false}) searchInputField: ElementRef;
@ViewChild(MatPaginator, {static: false}) paginator: MatPaginator;
@ViewChild(MatSort, {static: false}) sort: MatSort;
constructor(protected store: Store<AppState>,
private route: ActivatedRoute,
public translate: TranslateService,
public dialog: MatDialog,
private dialogService: DialogService,
private domSanitizer: DomSanitizer,
private componentFactoryResolver: ComponentFactoryResolver) {
super(store);
}
ngOnInit() {
this.entitiesTableConfig = this.entitiesTableConfig || this.route.snapshot.data.entitiesTableConfig;
if (this.entitiesTableConfig.headerComponent) {
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.entitiesTableConfig.headerComponent);
const viewContainerRef = this.entityTableHeaderAnchor.viewContainerRef;
viewContainerRef.clear();
const componentRef = viewContainerRef.createComponent(componentFactory);
const headerComponent = componentRef.instance;
headerComponent.entitiesTableConfig = this.entitiesTableConfig;
}
this.entitiesTableConfig.table = this;
this.translations = this.entitiesTableConfig.entityTranslations;
this.headerActionDescriptors = [...this.entitiesTableConfig.headerActionDescriptors];
this.groupActionDescriptors = [...this.entitiesTableConfig.groupActionDescriptors];
this.cellActionDescriptors = [...this.entitiesTableConfig.cellActionDescriptors];
if (this.entitiesTableConfig.entitiesDeleteEnabled) {
this.cellActionDescriptors.push(
{
name: this.translate.instant('action.delete'),
icon: 'delete',
isEnabled: entity => this.entitiesTableConfig.deleteEnabled(entity),
onAction: ($event, entity) => this.deleteEntity($event, entity)
}
);
}
this.groupActionDescriptors.push(
{
name: this.translate.instant('action.delete'),
icon: 'delete',
isEnabled: this.entitiesTableConfig.entitiesDeleteEnabled,
onAction: ($event, entities) => this.deleteEntities($event, entities)
}
);
this.columns = [...this.entitiesTableConfig.columns];
this.selectionEnabled = this.entitiesTableConfig.selectionEnabled;
if (this.selectionEnabled) {
this.displayedColumns.push('select');
}
this.columns.forEach(
(column) => {
this.displayedColumns.push(column.key);
}
);
this.displayedColumns.push('actions');
const sortOrder: SortOrder = { property: this.entitiesTableConfig.defaultSortOrder.property,
direction: this.entitiesTableConfig.defaultSortOrder.direction };
if (this.entitiesTableConfig.useTimePageLink) {
this.timewindow = Timewindow.historyInterval(24 * 60 * 60 * 1000);
const currentTime = new Date().getTime();
this.pageLink = new TimePageLink(10, 0, null, sortOrder,
currentTime - this.timewindow.history.timewindowMs, currentTime);
} else {
this.pageLink = new PageLink(10, 0, null, sortOrder);
}
this.dataSource = new EntitiesDataSource<BaseData<HasId>>(
this.entitiesTableConfig.entitiesFetchFunction
);
if (this.entitiesTableConfig.onLoadAction) {
this.entitiesTableConfig.onLoadAction(this.route);
}
if (this.entitiesTableConfig.loadDataOnInit) {
this.dataSource.loadEntities(this.pageLink);
}
}
ngAfterViewInit() {
fromEvent(this.searchInputField.nativeElement, 'keyup')
.pipe(
debounceTime(150),
distinctUntilChanged(),
tap(() => {
this.paginator.pageIndex = 0;
this.updateData();
})
)
.subscribe();
this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0);
merge(this.sort.sortChange, this.paginator.page)
.pipe(
tap(() => this.updateData())
)
.subscribe();
}
addEnabled() {
return this.entitiesTableConfig.addEnabled;
}
updateData(closeDetails: boolean = true) {
if (closeDetails) {
this.isDetailsOpen = false;
}
this.pageLink.page = this.paginator.pageIndex;
this.pageLink.pageSize = this.paginator.pageSize;
this.pageLink.sortOrder.property = this.sort.active;
this.pageLink.sortOrder.direction = Direction[this.sort.direction.toUpperCase()];
if (this.entitiesTableConfig.useTimePageLink) {
const timePageLink = this.pageLink as TimePageLink;
if (this.timewindow.history.timewindowMs) {
const currentTime = new Date().getTime();
timePageLink.startTime = currentTime - this.timewindow.history.timewindowMs;
timePageLink.endTime = currentTime;
} else {
timePageLink.startTime = this.timewindow.history.fixedTimewindow.startTimeMs;
timePageLink.endTime = this.timewindow.history.fixedTimewindow.endTimeMs;
}
}
this.dataSource.loadEntities(this.pageLink);
}
onRowClick($event: Event, entity) {
if ($event) {
$event.stopPropagation();
}
if (this.dataSource.toggleCurrentEntity(entity)) {
this.isDetailsOpen = true;
} else {
this.isDetailsOpen = !this.isDetailsOpen;
}
}
addEntity($event: Event) {
let entity$: Observable<BaseData<HasId>>;
if (this.entitiesTableConfig.addEntity) {
entity$ = this.entitiesTableConfig.addEntity();
} else {
entity$ = this.dialog.open<AddEntityDialogComponent, AddEntityDialogData<BaseData<HasId>>,
BaseData<HasId>>(AddEntityDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
entitiesTableConfig: this.entitiesTableConfig
}
}).afterClosed();
}
entity$.subscribe(
(entity) => {
if (entity) {
this.updateData();
}
}
);
}
onEntityUpdated(entity: BaseData<HasId>) {
this.updateData(false);
}
onEntityAction(action: EntityAction<BaseData<HasId>>) {
if (action.action === 'delete') {
this.deleteEntity(action.event, action.entity);
}
}
deleteEntity($event: Event, entity: BaseData<HasId>) {
if ($event) {
$event.stopPropagation();
}
this.dialogService.confirm(
this.entitiesTableConfig.deleteEntityTitle(entity),
this.entitiesTableConfig.deleteEntityContent(entity),
this.translate.instant('action.no'),
this.translate.instant('action.yes'),
true
).subscribe((result) => {
if (result) {
this.entitiesTableConfig.deleteEntity(entity.id).subscribe(
() => {
this.updateData();
}
);
}
});
}
deleteEntities($event: Event, entities: BaseData<HasId>[]) {
if ($event) {
$event.stopPropagation();
}
this.dialogService.confirm(
this.entitiesTableConfig.deleteEntitiesTitle(entities.length),
this.entitiesTableConfig.deleteEntitiesContent(entities.length),
this.translate.instant('action.no'),
this.translate.instant('action.yes'),
true
).subscribe((result) => {
if (result) {
const tasks: Observable<any>[] = [];
entities.forEach((entity) => {
if (this.entitiesTableConfig.deleteEnabled(entity)) {
tasks.push(this.entitiesTableConfig.deleteEntity(entity.id));
}
});
forkJoin(tasks).subscribe(
() => {
this.updateData();
}
);
}
});
}
onTimewindowChange() {
this.updateData();
}
enterFilterMode() {
this.textSearchMode = true;
this.pageLink.textSearch = '';
setTimeout(() => {
this.searchInputField.nativeElement.focus();
this.searchInputField.nativeElement.setSelectionRange(0, 0);
}, 10);
}
exitFilterMode() {
this.textSearchMode = false;
this.pageLink.textSearch = null;
this.paginator.pageIndex = 0;
this.updateData();
}
resetSortAndFilter(update: boolean = true) {
this.pageLink.textSearch = null;
if (this.entitiesTableConfig.useTimePageLink) {
this.timewindow = Timewindow.historyInterval(24 * 60 * 60 * 1000);
}
this.paginator.pageIndex = 0;
const sortable = this.sort.sortables.get(this.entitiesTableConfig.defaultSortOrder.property);
this.sort.active = sortable.id;
this.sort.direction = this.entitiesTableConfig.defaultSortOrder.direction === Direction.ASC ? 'asc' : 'desc';
if (update) {
this.updateData();
}
}
cellContent(entity: BaseData<HasId>, column: EntityTableColumn<BaseData<HasId>>) {
return this.domSanitizer.bypassSecurityTrustHtml(column.cellContentFunction(entity, column.key));
}
cellStyle(entity: BaseData<HasId>, column: EntityTableColumn<BaseData<HasId>>) {
return {...column.cellStyleFunction(entity, column.key), ...{maxWidth: column.maxWidth}};
}
}

28
ui-ngx/src/app/shared/components/entity/entity-component.models.ts

@ -0,0 +1,28 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { BaseData, HasId } from '@shared/models/base-data';
import { EntityTableConfig } from '@shared/components/entity/entities-table-config.models';
export interface AddEntityDialogData<T extends BaseData<HasId>> {
entitiesTableConfig: EntityTableConfig<T>;
}
export interface EntityAction<T extends BaseData<HasId>> {
event: Event;
action: string;
entity: T;
}

43
ui-ngx/src/app/shared/components/entity/entity-details-panel.component.html

@ -0,0 +1,43 @@
<!--
Copyright © 2016-2019 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<tb-details-panel fxFlex
[headerTitle]="entity?.name"
headerSubtitle="{{ translations.details | translate }}"
[isReadOnly]="entitiesTableConfig.detailsReadonly(entity)"
[isEdit]="isEditValue"
(closeDetails)="onCloseEntityDetails()"
(toggleDetailsEditMode)="onToggleEditMode($event)"
(applyDetails)="saveEntity()"
[theForm]="detailsForm">
<div class="details-buttons">
<div [tb-help]="resources.helpLinkId"></div>
</div>
<mat-tab-group class="tb-absolute-fill" [ngClass]="{'tb-headless': isEditValue}" fxFlex [(selectedIndex)]="selectedTab">
<mat-tab label="{{ 'details.details' | translate }}">
<tb-anchor #entityDetailsForm></tb-anchor>
</mat-tab>
<!--mat-tab *ngIf="entity && entitiesTableConfig.entityType !== entityTypes.CUSTOMER"
label="{{ 'audit-log.audit-logs' | translate }}">
<tb-audit-log-table [active]="selectedTab === 1" [auditLogMode]="auditLogModes.ENTITY" [entityId]="entity.id" detailsMode="true"></tb-audit-log-table>
</mat-tab>
<mat-tab *ngIf="entity && entitiesTableConfig.entityType === entityTypes.CUSTOMER"
label="{{ 'audit-log.audit-logs' | translate }}">
<tb-audit-log-table [active]="selectedTab === 1" [auditLogMode]="auditLogModes.CUSTOMER" [customerId]="entity.id" detailsMode="true"></tb-audit-log-table>
</mat-tab-->
</mat-tab-group>
</tb-details-panel>

31
ui-ngx/src/app/shared/components/entity/entity-details-panel.component.scss

@ -0,0 +1,31 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
:host ::ng-deep {
.mat-tab-body-wrapper {
position: absolute;
top: 49px;
left: 0;
right: 0;
bottom: 0;
}
}

165
ui-ngx/src/app/shared/components/entity/entity-details-panel.component.ts

@ -0,0 +1,165 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
Component,
ComponentFactoryResolver,
EventEmitter,
Input,
OnDestroy,
OnInit,
Output,
ViewChild
} from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { EntityTableConfig } from '@shared/components/entity/entities-table-config.models';
import { BaseData, HasId } from '@shared/models/base-data';
import {
EntityType,
EntityTypeResource,
EntityTypeTranslation
} from '@shared/models/entity-type.models';
import { NgForm } from '@angular/forms';
import { EntityComponent } from '@shared/components/entity/entity.component';
import { TbAnchorComponent } from '@shared/components/tb-anchor.component';
import { EntityAction } from '@shared/components/entity/entity-component.models';
import { Subscription } from 'rxjs';
// import { AuditLogMode } from '@shared/models/audit-log.models';
@Component({
selector: 'tb-entity-details-panel',
templateUrl: './entity-details-panel.component.html',
styleUrls: ['./entity-details-panel.component.scss']
})
export class EntityDetailsPanelComponent extends PageComponent implements OnInit, OnDestroy {
@Input() entitiesTableConfig: EntityTableConfig<BaseData<HasId>>;
@Output()
closeEntityDetails = new EventEmitter<void>();
@Output()
entityUpdated = new EventEmitter<BaseData<HasId>>();
@Output()
entityAction = new EventEmitter<EntityAction<BaseData<HasId>>>();
entityComponent: EntityComponent<BaseData<HasId>>;
detailsForm: NgForm;
isEditValue = false;
selectedTab = 0;
entityTypes = EntityType;
@ViewChild('entityDetailsForm', {static: true}) entityDetailsFormAnchor: TbAnchorComponent;
translations: EntityTypeTranslation;
resources: EntityTypeResource;
entity: BaseData<HasId>;
private currentEntityId: HasId;
private entityActionSubscription: Subscription;
constructor(protected store: Store<AppState>,
private componentFactoryResolver: ComponentFactoryResolver) {
super(store);
}
@Input()
set entityId(entityId: HasId) {
if (entityId && entityId !== this.currentEntityId) {
this.currentEntityId = entityId;
this.reload();
}
}
set isEdit(val: boolean) {
this.isEditValue = val;
this.entityComponent.isEdit = val;
}
get isEdit() {
return this.isEditValue;
}
ngOnInit(): void {
this.translations = this.entitiesTableConfig.entityTranslations;
this.resources = this.entitiesTableConfig.entityResources;
this.buildEntityComponent();
}
ngOnDestroy(): void {
super.ngOnDestroy();
if (this.entityActionSubscription) {
this.entityActionSubscription.unsubscribe();
}
}
buildEntityComponent() {
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.entitiesTableConfig.entityComponent);
const viewContainerRef = this.entityDetailsFormAnchor.viewContainerRef;
viewContainerRef.clear();
const componentRef = viewContainerRef.createComponent(componentFactory);
this.entityComponent = componentRef.instance;
this.entityComponent.isEdit = this.isEdit;
this.entityComponent.entitiesTableConfig = this.entitiesTableConfig;
this.detailsForm = this.entityComponent.entityNgForm;
this.entityActionSubscription = this.entityComponent.entityAction.subscribe((action) => {
this.entityAction.emit(action);
});
}
reload(): void {
this.isEdit = false;
this.entitiesTableConfig.loadEntity(this.currentEntityId).subscribe(
(entity) => {
this.entity = entity;
this.entityComponent.entity = entity;
}
);
}
onCloseEntityDetails() {
this.closeEntityDetails.emit();
}
onToggleEditMode(isEdit: boolean) {
this.isEdit = isEdit;
if (!this.isEdit) {
this.entityComponent.entity = this.entity;
} else {
this.selectedTab = 0;
}
}
saveEntity() {
if (this.detailsForm.valid) {
const editingEntity = {...this.entity, ...this.entityComponent.entityFormValue()};
this.entitiesTableConfig.saveEntity(editingEntity).subscribe(
(entity) => {
this.entity = entity;
this.entityComponent.entity = entity;
this.isEdit = false;
this.entityUpdated.emit(this.entity);
}
);
}
}
}

36
ui-ngx/src/app/shared/components/entity/entity-table-header.component.ts

@ -0,0 +1,36 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { BaseData, HasId } from '@shared/models/base-data';
import { PageComponent } from '@shared/components/page.component';
import { Input, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { EntityTableConfig } from '@shared/components/entity/entities-table-config.models';
export abstract class EntityTableHeaderComponent<T extends BaseData<HasId>> extends PageComponent implements OnInit {
@Input()
entitiesTableConfig: EntityTableConfig<T>;
protected constructor(protected store: Store<AppState>) {
super(store);
}
ngOnInit() {
}
}

110
ui-ngx/src/app/shared/components/entity/entity.component.ts

@ -0,0 +1,110 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { BaseData, HasId } from '@shared/models/base-data';
import { FormGroup, NgForm } from '@angular/forms';
import { PageComponent } from '@shared/components/page.component';
import { EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { EntityAction } from '@shared/components/entity/entity-component.models';
import { EntityTableConfig } from '@shared/components/entity/entities-table-config.models';
export abstract class EntityComponent<T extends BaseData<HasId>> extends PageComponent implements OnInit {
entityValue: T;
entityForm: FormGroup;
@ViewChild('entityNgForm', {static: true}) entityNgForm: NgForm;
isEditValue: boolean;
@Input()
set isEdit(isEdit: boolean) {
this.isEditValue = isEdit;
this.updateFormState();
}
get isEdit() {
return this.isEditValue;
}
get isAdd(): boolean {
return this.entityValue && !this.entityValue.id;
}
@Input()
set entity(entity: T) {
this.entityValue = entity;
if (this.entityForm) {
this.entityForm.reset();
this.updateForm(entity);
}
}
get entity(): T {
return this.entityValue;
}
@Input()
entitiesTableConfig: EntityTableConfig<T>;
@Output()
entityAction = new EventEmitter<EntityAction<T>>();
protected constructor(protected store: Store<AppState>) {
super(store);
}
ngOnInit() {
this.entityForm = this.buildForm(this.entityValue);
}
onEntityAction($event: Event, action: string) {
const entityAction = {event: $event, action, entity: this.entity} as EntityAction<T>;
let handled = false;
if (this.entitiesTableConfig) {
handled = this.entitiesTableConfig.onEntityAction(entityAction);
}
if (!handled) {
this.entityAction.emit(entityAction);
}
}
updateFormState() {
if (this.entityForm) {
if (this.isEditValue) {
this.entityForm.enable({emitEvent: false});
} else {
this.entityForm.disable({emitEvent: false});
}
}
}
entityFormValue() {
const formValue = this.entityForm ? {...this.entityForm.value} : {};
return this.prepareFormValue(formValue);
}
prepareFormValue(formValue: any): any {
return formValue;
}
abstract buildForm(entity: T): FormGroup;
abstract updateForm(entity: T);
}

47
ui-ngx/src/app/shared/components/time/datetime-period.component.html

@ -0,0 +1,47 @@
<!--
Copyright © 2016-2019 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<section fxLayout="column" fxLayoutAlign="start start">
<section fxLayout="row" fxLayoutAlign="start start" fxLayoutGap="16px">
<mat-form-field>
<mat-placeholder translate>datetime.date-from</mat-placeholder>
<mat-datetimepicker-toggle [for]="startDatePicker" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #startDatePicker type="date" openOnFocus="true"></mat-datetimepicker>
<input matInput [(ngModel)]="startDate" [matDatetimepicker]="startDatePicker" (ngModelChange)="onStartDateChange()">
</mat-form-field>
<mat-form-field>
<mat-placeholder translate>datetime.time-from</mat-placeholder>
<mat-datetimepicker-toggle [for]="startTimePicker" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #startTimePicker type="time" openOnFocus="true"></mat-datetimepicker>
<input matInput [(ngModel)]="startDate" [matDatetimepicker]="startTimePicker" (ngModelChange)="onStartDateChange()">
</mat-form-field>
</section>
<section fxLayout="row" fxLayoutAlign="start start" fxLayoutGap="16px">
<mat-form-field>
<mat-placeholder translate>datetime.date-to</mat-placeholder>
<mat-datetimepicker-toggle [for]="endDatePicker" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #endDatePicker type="date" openOnFocus="true"></mat-datetimepicker>
<input matInput [(ngModel)]="endDate" [matDatetimepicker]="endDatePicker" (ngModelChange)="onEndDateChange()">
</mat-form-field>
<mat-form-field>
<mat-placeholder translate>datetime.time-to</mat-placeholder>
<mat-datetimepicker-toggle [for]="endTimePicker" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #endTimePicker type="time" openOnFocus="true"></mat-datetimepicker>
<input matInput [(ngModel)]="endDate" [matDatetimepicker]="endTimePicker" (ngModelChange)="onEndDateChange()">
</mat-form-field>
</section>
</section>

26
ui-ngx/src/app/shared/components/time/datetime-period.component.scss

@ -0,0 +1,26 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host ::ng-deep {
.mat-form-field-wrapper {
padding-bottom: 8px;
}
.mat-form-field-underline {
bottom: 8px;
}
.mat-form-field-infix {
width: 150px;
}
}

137
ui-ngx/src/app/shared/components/time/datetime-period.component.ts

@ -0,0 +1,137 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { FixedWindow } from '@shared/models/time/time.models';
@Component({
selector: 'tb-datetime-period',
templateUrl: './datetime-period.component.html',
styleUrls: ['./datetime-period.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DatetimePeriodComponent),
multi: true
}
]
})
export class DatetimePeriodComponent implements OnInit, ControlValueAccessor {
@Input() disabled: boolean;
modelValue: FixedWindow;
startDate: Date;
endDate: Date;
endTime: any;
maxStartDate: Date;
minEndDate: Date;
maxEndDate: Date;
changePending = false;
private propagateChange = null;
constructor() {
}
ngOnInit(): void {
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
if (this.changePending && this.propagateChange) {
this.changePending = false;
this.propagateChange(this.modelValue);
}
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}
writeValue(datePeriod: FixedWindow): void {
this.modelValue = datePeriod;
if (this.modelValue) {
this.startDate = new Date(this.modelValue.startTimeMs);
this.endDate = new Date(this.modelValue.endTimeMs);
} else {
const date = new Date();
this.startDate = new Date(
date.getFullYear(),
date.getMonth(),
date.getDate() - 1,
date.getHours(),
date.getMinutes(),
date.getSeconds(),
date.getMilliseconds());
this.endDate = date;
this.updateView();
}
this.updateMinMaxDates();
}
updateView() {
let value: FixedWindow = null;
if (this.startDate && this.endDate) {
value = new FixedWindow();
value.startTimeMs = this.startDate.getTime();
value.endTimeMs = this.endDate.getTime();
}
this.modelValue = value;
if (!this.propagateChange) {
this.changePending = true;
} else {
this.propagateChange(this.modelValue);
}
}
updateMinMaxDates() {
this.maxStartDate = new Date(this.endDate.getTime() - 1000);
this.minEndDate = new Date(this.startDate.getTime() + 1000);
this.maxEndDate = new Date();
}
onStartDateChange() {
if (this.startDate) {
if (this.startDate.getTime() > this.maxStartDate.getTime()) {
this.startDate = new Date(this.maxStartDate.getTime());
}
this.updateMinMaxDates();
}
this.updateView();
}
onEndDateChange() {
if (this.endDate) {
if (this.endDate.getTime() < this.minEndDate.getTime()) {
this.endDate = new Date(this.minEndDate.getTime());
} else if (this.endDate.getTime() > this.maxEndDate.getTime()) {
this.endDate = new Date(this.maxEndDate.getTime());
}
this.updateMinMaxDates();
}
this.updateView();
}
}

54
ui-ngx/src/app/shared/components/time/timeinterval.component.html

@ -0,0 +1,54 @@
<!--
Copyright © 2016-2019 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<section fxLayout="row">
<section class="interval-section" fxLayout="column" fxFlex [fxShow]="advanced">
<label class="tb-small interval-label" translate>{{ predefinedName }}</label>
<section fxLayout="row" fxLayoutAlign="start start" fxFlex fxLayoutGap="6px">
<mat-form-field class="number-input">
<mat-label translate>timeinterval.days</mat-label>
<input matInput type="number" step="1" min="0" [(ngModel)]="days" (ngModelChange)="onTimeInputChange('days')"/>
</mat-form-field>
<mat-form-field class="number-input">
<mat-label translate>timeinterval.hours</mat-label>
<input matInput type="number" step="1" [(ngModel)]="hours" (ngModelChange)="onTimeInputChange('hours')"/>
</mat-form-field>
<mat-form-field class="number-input">
<mat-label translate>timeinterval.minutes</mat-label>
<input matInput type="number" step="1" [(ngModel)]="mins" (ngModelChange)="onTimeInputChange('mins')"/>
</mat-form-field>
<mat-form-field class="number-input">
<mat-label translate>timeinterval.seconds</mat-label>
<input matInput type="number" step="1" [(ngModel)]="secs" (ngModelChange)="onTimeInputChange('secs')"/>
</mat-form-field>
</section>
</section>
<section class="interval-section" fxLayout="row" fxFlex [fxShow]="!advanced">
<mat-form-field fxFlex>
<mat-label translate>{{ predefinedName }}</mat-label>
<mat-select matInput [(ngModel)]="intervalMs" (ngModelChange)="onIntervalMsChange()" style="min-width: 150px;">
<mat-option *ngFor="let interval of intervals" [value]="interval.value">
{{ interval.name | translate:interval.translateParams }}
</mat-option>
</mat-select>
</mat-form-field>
</section>
<section fxLayout="column" fxLayoutAlign="center center">
<label class="tb-small advanced-label" translate>timeinterval.advanced</label>
<mat-slide-toggle class="advanced-switch" [(ngModel)]="advanced" (ngModelChange)="onAdvancedChange()"></mat-slide-toggle>
</section>
</section>

43
ui-ngx/src/app/shared/components/time/timeinterval.component.scss

@ -0,0 +1,43 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
min-width: 355px;
.advanced-switch {
margin-bottom: 16px;
}
.advanced-label {
margin: 5px 0;
}
.interval-section {
min-height: 66px;
.interval-label {
margin-bottom: 7px;
margin-top: -1px;
}
}
}
:host ::ng-deep {
.number-input {
.mat-form-field-infix {
width: 70px;
}
}
}

277
ui-ngx/src/app/shared/components/time/timeinterval.component.ts

@ -0,0 +1,277 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { ChangeDetectorRef, Component, forwardRef, Input, OnInit } from '@angular/core';
import {
ControlValueAccessor,
FormControl,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
Validator
} from '@angular/forms';
import { Timewindow } from '@shared/models/time/time.models';
import { TimeInterval, TimeService } from '@core/services/time.service';
@Component({
selector: 'tb-timeinterval',
templateUrl: './timeinterval.component.html',
styleUrls: ['./timeinterval.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => TimeintervalComponent),
multi: true
}
]
})
export class TimeintervalComponent implements OnInit, ControlValueAccessor {
minValue: number;
maxValue: number;
@Input()
set min(min: number) {
if (typeof min !== 'undefined' && min !== this.minValue) {
this.minValue = min;
this.updateView();
}
}
@Input()
set max(max: number) {
if (typeof max !== 'undefined' && max !== this.maxValue) {
this.maxValue = max;
this.updateView();
}
}
@Input() predefinedName: string;
@Input() disabled: boolean;
days = 0;
hours = 0;
mins = 1;
secs = 0;
intervalMs = 0;
modelValue: number;
advanced = false;
rendered = false;
intervals: Array<TimeInterval>;
private propagateChange = (_: any) => {};
constructor(private timeService: TimeService) {
}
ngOnInit(): void {
this.boundInterval();
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}
writeValue(intervalMs: number): void {
this.modelValue = intervalMs;
this.rendered = true;
if (typeof this.modelValue !== 'undefined') {
const min = this.timeService.boundMinInterval(this.minValue);
const max = this.timeService.boundMaxInterval(this.maxValue);
if (this.modelValue >= min && this.modelValue <= max) {
this.advanced = !this.timeService.matchesExistingInterval(this.minValue, this.maxValue, this.modelValue);
this.setIntervalMs(this.modelValue);
} else {
this.boundInterval();
}
}
}
setIntervalMs(intervalMs: number) {
if (!this.advanced) {
this.intervalMs = intervalMs;
}
const intervalSeconds = Math.floor(intervalMs / 1000);
this.days = Math.floor(intervalSeconds / 86400);
this.hours = Math.floor((intervalSeconds % 86400) / 3600);
this.mins = Math.floor(((intervalSeconds % 86400) % 3600) / 60);
this.secs = intervalSeconds % 60;
}
boundInterval() {
const min = this.timeService.boundMinInterval(this.minValue);
const max = this.timeService.boundMaxInterval(this.maxValue);
this.intervals = this.timeService.getIntervals(this.minValue, this.maxValue);
if (this.rendered) {
let newIntervalMs = this.modelValue;
if (newIntervalMs < min) {
newIntervalMs = min;
} else if (newIntervalMs > max) {
newIntervalMs = max;
}
if (!this.advanced) {
newIntervalMs = this.timeService.boundToPredefinedInterval(min, max, newIntervalMs);
}
if (newIntervalMs !== this.modelValue) {
this.setIntervalMs(newIntervalMs);
this.updateView();
}
}
}
updateView() {
if (!this.rendered) {
return;
}
let value = null;
let intervalMs;
if (!this.advanced) {
intervalMs = this.intervalMs;
if (!intervalMs || isNaN(intervalMs)) {
intervalMs = this.calculateIntervalMs();
}
} else {
intervalMs = this.calculateIntervalMs();
}
if (!isNaN(intervalMs) && intervalMs > 0) {
value = intervalMs;
}
this.modelValue = value;
this.propagateChange(this.modelValue);
this.boundInterval();
}
calculateIntervalMs(): number {
return (this.days * 86400 +
this.hours * 3600 +
this.mins * 60 +
this.secs) * 1000;
}
onIntervalMsChange() {
this.updateView();
}
onAdvancedChange() {
if (!this.advanced) {
this.intervalMs = this.calculateIntervalMs();
} else {
let intervalMs = this.intervalMs;
if (!intervalMs || isNaN(intervalMs)) {
intervalMs = this.calculateIntervalMs();
}
this.setIntervalMs(intervalMs);
}
this.updateView();
}
onTimeInputChange(type: string) {
switch (type) {
case 'secs':
setTimeout(() => this.onSecsChange(), 0);
break;
case 'mins':
setTimeout(() => this.onMinsChange(), 0);
break;
case 'hours':
setTimeout(() => this.onHoursChange(), 0);
break;
case 'days':
setTimeout(() => this.onDaysChange(), 0);
break;
}
}
onSecsChange() {
if (typeof this.secs === 'undefined') {
return;
}
if (this.secs < 0) {
if ((this.days + this.hours + this.mins) > 0) {
this.secs = this.secs + 60;
this.mins--;
this.onMinsChange();
} else {
this.secs = 0;
}
} else if (this.secs >= 60) {
this.secs = this.secs - 60;
this.mins++;
this.onMinsChange();
}
this.updateView();
}
onMinsChange() {
if (typeof this.mins === 'undefined') {
return;
}
if (this.mins < 0) {
if ((this.days + this.hours) > 0) {
this.mins = this.mins + 60;
this.hours--;
this.onHoursChange();
} else {
this.mins = 0;
}
} else if (this.mins >= 60) {
this.mins = this.mins - 60;
this.hours++;
this.onHoursChange();
}
this.updateView();
}
onHoursChange() {
if (typeof this.hours === 'undefined') {
return;
}
if (this.hours < 0) {
if (this.days > 0) {
this.hours = this.hours + 24;
this.days--;
this.onDaysChange();
} else {
this.hours = 0;
}
} else if (this.hours >= 24) {
this.hours = this.hours - 24;
this.days++;
this.onDaysChange();
}
this.updateView();
}
onDaysChange() {
if (typeof this.days === 'undefined') {
return;
}
if (this.days < 0) {
this.days = 0;
}
this.updateView();
}
}

127
ui-ngx/src/app/shared/components/time/timewindow-panel.component.html

@ -0,0 +1,127 @@
<!--
Copyright © 2016-2019 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<form [formGroup]="timewindowForm" (ngSubmit)="update()">
<fieldset [disabled]="(isLoading$ | async)">
<div class="mat-content" style="height: 100%;" fxFlex fxLayout="column">
<section fxLayout="column">
<mat-tab-group dynamicHeight [ngClass]="{'tb-headless': historyOnly}"
(selectedIndexChange)="timewindowForm.markAsDirty()" [(selectedIndex)]="timewindow.selectedTab">
<mat-tab label="{{ 'timewindow.realtime' | translate }}">
<div formGroupName="realtime" class="mat-content mat-padding" fxLayout="column">
<tb-timeinterval
formControlName="timewindowMs"
predefinedName="timewindow.last"
[required]="timewindow.selectedTab === timewindowTypes.REALTIME"
style="padding-top: 8px;"></tb-timeinterval>
</div>
</mat-tab>
<mat-tab label="{{ 'timewindow.history' | translate }}">
<div formGroupName="history" class="mat-content mat-padding" style="padding-top: 8px;">
<mat-radio-group formControlName="historyType">
<mat-radio-button [value]="historyTypes.LAST_INTERVAL" color="primary">
<section fxLayout="column">
<tb-timeinterval
formControlName="timewindowMs"
predefinedName="timewindow.last"
[fxShow]="timewindowForm.get('history').get('historyType').value === historyTypes.LAST_INTERVAL"
[required]="timewindow.selectedTab === timewindowTypes.HISTORY &&
timewindowForm.get('history').get('historyType').value === historyTypes.LAST_INTERVAL"
style="padding-top: 8px;"></tb-timeinterval>
</section>
</mat-radio-button>
<mat-radio-button [value]="historyTypes.FIXED" color="primary">
<section fxLayout="column">
<span translate>timewindow.time-period</span>
<tb-datetime-period
formControlName="fixedTimewindow"
[fxShow]="timewindowForm.get('history').get('historyType').value === historyTypes.FIXED"
[required]="timewindow.selectedTab === timewindowTypes.HISTORY &&
timewindowForm.get('history').get('historyType').value === historyTypes.FIXED"
style="padding-top: 8px;"></tb-datetime-period>
</section>
</mat-radio-button>
</mat-radio-group>
</div>
</mat-tab>
</mat-tab-group>
<div *ngIf="aggregation" formGroupName="aggregation" class="mat-content mat-padding" fxLayout="column">
<mat-form-field>
<mat-label translate>aggregation.function</mat-label>
<mat-select matInput formControlName="type" style="min-width: 150px;">
<mat-option *ngFor="let aggregation of aggregations" [value]="aggregation">
{{ aggregationTypesTranslations.get(aggregation) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<div *ngIf="timewindowForm.get('aggregation').get('type').value === aggregationTypes.NONE"
class="limit-slider-container"
fxLayout="row" fxLayoutAlign="start center">
<span translate>aggregation.limit</span>
<mat-slider fxFlex formControlName="limit"
thumbLabel
[value]="timewindowForm.get('aggregation').get('limit').value"
min="{{minDatapointsLimit()}}"
max="{{maxDatapointsLimit()}}">
</mat-slider>
<mat-form-field style="max-width: 80px;">
<input matInput formControlName="limit" type="number" step="1"
[value]="timewindowForm.get('aggregation').get('limit').value"
min="{{minDatapointsLimit()}}"
max="{{maxDatapointsLimit()}}"/>
</mat-form-field>
</div>
</div>
<div formGroupName="realtime"
*ngIf="aggregation && timewindowForm.get('aggregation').get('type').value !== aggregationTypes.NONE &&
timewindow.selectedTab === timewindowTypes.REALTIME" class="mat-content mat-padding" fxLayout="column">
<tb-timeinterval
formControlName="interval"
[min]="minRealtimeAggInterval()" [max]="maxRealtimeAggInterval()"
predefinedName="aggregation.group-interval">
</tb-timeinterval>
</div>
<div formGroupName="history"
*ngIf="aggregation && timewindowForm.get('aggregation').get('type').value !== aggregationTypes.NONE &&
timewindow.selectedTab === timewindowTypes.HISTORY" class="mat-content mat-padding" fxLayout="column">
<tb-timeinterval
formControlName="interval"
[min]="minHistoryAggInterval()" [max]="maxHistoryAggInterval()"
predefinedName="aggregation.group-interval">
</tb-timeinterval>
</div>
</section>
<span fxFlex></span>
<div fxLayout="row" class="tb-panel-actions">
<span fxFlex></span>
<button type="submit"
mat-raised-button
color="primary"
[disabled]="(isLoading$ | async) || timewindowForm.invalid || !timewindowForm.dirty">
{{ 'action.update' | translate }}
</button>
<button type="button"
mat-button
[disabled]="(isLoading$ | async)"
(click)="cancel()"
style="margin-right: 20px;">
{{ 'action.cancel' | translate }}
</button>
</div>
</div>
</fieldset>
</form>

63
ui-ngx/src/app/shared/components/time/timewindow-panel.component.scss

@ -0,0 +1,63 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
width: 100%;
height: 100%;
form,
fieldset {
height: 100%;
}
.mat-content {
overflow: hidden;
background-color: #fff;
}
.mat-padding {
padding: 0 16px;
}
.limit-slider-container {
>:first-child {
margin-right: 16px;
}
>:last-child {
margin-left: 16px;
}
>:first-child, >:last-child {
min-width: 25px;
max-width: 42px;
}
mat-form-field input[type=number] {
text-align: center;
}
}
}
:host ::ng-deep {
mat-radio-button {
display: block;
margin-bottom: 16px;
.mat-radio-label {
width: 100%;
align-items: start;
.mat-radio-label-content {
width: 100%;
}
}
}
}

203
ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts

@ -0,0 +1,203 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
Component,
Inject,
InjectionToken,
OnInit,
ViewChild,
ViewContainerRef
} from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { MillisecondsToTimeStringPipe } from '@shared/pipe/milliseconds-to-time-string.pipe';
import {
Aggregation,
aggregationTranslations,
AggregationType,
HistoryWindow,
HistoryWindowType,
IntervalWindow,
Timewindow,
TimewindowType
} from '@shared/models/time/time.models';
import { DatePipe } from '@angular/common';
import { Overlay, OverlayRef } from '@angular/cdk/overlay';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { TimeService } from '@core/services/time.service';
export const TIMEWINDOW_PANEL_DATA = new InjectionToken<any>('TimewindowPanelData');
export interface TimewindowPanelData {
historyOnly: boolean;
timewindow: Timewindow;
aggregation: boolean;
}
@Component({
selector: 'tb-timewindow-panel',
templateUrl: './timewindow-panel.component.html',
styleUrls: ['./timewindow-panel.component.scss']
})
export class TimewindowPanelComponent extends PageComponent implements OnInit {
historyOnly = false;
aggregation = false;
timewindow: Timewindow;
result: Timewindow;
timewindowForm: FormGroup;
historyTypes = HistoryWindowType;
timewindowTypes = TimewindowType;
aggregationTypes = AggregationType;
aggregations = Object.keys(AggregationType);
aggregationTypesTranslations = aggregationTranslations;
constructor(@Inject(TIMEWINDOW_PANEL_DATA) public data: TimewindowPanelData,
public overlayRef: OverlayRef,
protected store: Store<AppState>,
public fb: FormBuilder,
private timeService: TimeService,
private translate: TranslateService,
private millisecondsToTimeStringPipe: MillisecondsToTimeStringPipe,
private datePipe: DatePipe,
private overlay: Overlay,
public viewContainerRef: ViewContainerRef) {
super(store);
this.historyOnly = data.historyOnly;
this.timewindow = data.timewindow;
this.aggregation = data.aggregation;
}
ngOnInit(): void {
this.timewindowForm = this.fb.group({
realtime: this.fb.group(
{
timewindowMs: [
this.timewindow.realtime && typeof this.timewindow.realtime.timewindowMs !== 'undefined'
? this.timewindow.realtime.timewindowMs : null
],
interval: [
this.timewindow.realtime && typeof this.timewindow.realtime.interval !== 'undefined'
? this.timewindow.realtime.interval : null
]
}
),
history: this.fb.group(
{
historyType: [
this.timewindow.history && typeof this.timewindow.history.historyType !== 'undefined'
? this.timewindow.history.historyType : HistoryWindowType.LAST_INTERVAL
],
timewindowMs: [
this.timewindow.history && typeof this.timewindow.history.timewindowMs !== 'undefined'
? this.timewindow.history.timewindowMs : null
],
interval: [
this.timewindow.history && typeof this.timewindow.history.interval !== 'undefined'
? this.timewindow.history.interval : null
],
fixedTimewindow: [
this.timewindow.history && typeof this.timewindow.history.fixedTimewindow !== 'undefined'
? this.timewindow.history.fixedTimewindow : null
]
}
),
aggregation: this.fb.group(
{
type: [
this.timewindow.aggregation && typeof this.timewindow.aggregation.type !== 'undefined'
? this.timewindow.aggregation.type : null
],
limit: [
this.timewindow.aggregation && typeof this.timewindow.aggregation.limit !== 'undefined'
? this.timewindow.aggregation.limit : null,
[Validators.min(this.minDatapointsLimit()), Validators.max(this.maxDatapointsLimit())]
]
}
)
});
}
update() {
const timewindowFormValue = this.timewindowForm.value;
this.timewindow.realtime = new IntervalWindow();
this.timewindow.realtime.timewindowMs = timewindowFormValue.realtime.timewindowMs;
this.timewindow.realtime.interval = timewindowFormValue.realtime.interval;
this.timewindow.history = new HistoryWindow();
this.timewindow.history.historyType = timewindowFormValue.history.historyType;
this.timewindow.history.timewindowMs = timewindowFormValue.history.timewindowMs;
this.timewindow.history.interval = timewindowFormValue.history.interval;
this.timewindow.history.fixedTimewindow = timewindowFormValue.history.fixedTimewindow;
if (this.aggregation) {
this.timewindow.aggregation = new Aggregation();
this.timewindow.aggregation.type = timewindowFormValue.aggregation.type;
this.timewindow.aggregation.limit = timewindowFormValue.aggregation.limit;
}
this.result = this.timewindow;
this.overlayRef.dispose();
}
cancel() {
this.overlayRef.dispose();
}
minDatapointsLimit() {
return this.timeService.getMinDatapointsLimit();
}
maxDatapointsLimit() {
return this.timeService.getMaxDatapointsLimit();
}
minRealtimeAggInterval() {
return this.timeService.minIntervalLimit(this.timewindowForm.get('realtime').get('timewindowMs').value);
}
maxRealtimeAggInterval() {
return this.timeService.maxIntervalLimit(this.timewindowForm.get('realtime').get('timewindowMs').value);
}
minHistoryAggInterval() {
return this.timeService.minIntervalLimit(this.currentHistoryTimewindow());
}
maxHistoryAggInterval() {
return this.timeService.maxIntervalLimit(this.currentHistoryTimewindow());
}
currentHistoryTimewindow() {
const timewindowFormValue = this.timewindowForm.value;
if (timewindowFormValue.history.historyType === HistoryWindowType.LAST_INTERVAL) {
return timewindowFormValue.history.timewindowMs;
} else {
return timewindowFormValue.history.fixedTimewindow.endTimeMs -
timewindowFormValue.history.fixedTimewindow.startTimeMs;
}
}
}

43
ui-ngx/src/app/shared/components/time/timewindow.component.html

@ -0,0 +1,43 @@
<!--
Copyright © 2016-2019 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<button *ngIf="asButton" cdkOverlayOrigin #timewindowPanelOrigin="cdkOverlayOrigin" [disabled]="disabled"
mat-raised-button color="primary" (click)="openEditMode($event)">
<mat-icon class="material-icons">query_builder</mat-icon>
<span>{{innerValue.displayValue}}</span>
</button>
<section *ngIf="!asButton" cdkOverlayOrigin #timewindowPanelOrigin="cdkOverlayOrigin"
class="tb-timewindow" fxLayout="row" fxLayoutAlign="start center">
<button *ngIf="direction === 'left'" [disabled]="disabled" mat-button mat-icon-button class="tb-mat-32"
(click)="openEditMode($event)"
matTooltip="{{ 'timewindow.edit' | translate }}"
[matTooltipPosition]="tooltipPosition">
<mat-icon class="material-icons">query_builder</mat-icon>
</button>
<span [fxHide]="hideLabel()"
(click)="openEditMode($event)"
matTooltip="{{ 'timewindow.edit' | translate }}"
[matTooltipPosition]="tooltipPosition">
{{innerValue.displayValue}}
</span>
<button *ngIf="direction === 'right'" [disabled]="disabled" mat-button mat-icon-button class="tb-mat-32"
(click)="openEditMode($event)"
matTooltip="{{ 'timewindow.edit' | translate }}"
[matTooltipPosition]="tooltipPosition">
<mat-icon class="material-icons">query_builder</mat-icon>
</button>
</section>

26
ui-ngx/src/app/shared/components/time/timewindow.component.scss

@ -0,0 +1,26 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
section.tb-timewindow {
span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
pointer-events: all;
cursor: pointer;
}
}
}

275
ui-ngx/src/app/shared/components/time/timewindow.component.ts

@ -0,0 +1,275 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
Component,
forwardRef, Inject,
Input,
OnDestroy,
OnInit,
ViewChild,
ViewContainerRef
} from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { TranslateService } from '@ngx-translate/core';
import { MillisecondsToTimeStringPipe } from '@shared/pipe/milliseconds-to-time-string.pipe';
import {
HistoryWindowType,
Timewindow,
TimewindowType
} from '@shared/models/time/time.models';
import { DatePipe } from '@angular/common';
import {
Overlay,
CdkOverlayOrigin,
OverlayConfig,
OverlayPositionBuilder, ConnectedPosition, PositionStrategy, OverlayRef
} from '@angular/cdk/overlay';
import {
TIMEWINDOW_PANEL_DATA,
TimewindowPanelComponent,
TimewindowPanelData
} from '@shared/components/time/timewindow-panel.component';
import { ComponentPortal, PortalInjector } from '@angular/cdk/portal';
import { MediaBreakpoints } from '@shared/models/constants';
import { BreakpointObserver } from '@angular/cdk/layout';
import { DOCUMENT } from '@angular/common';
import { WINDOW } from '@core/services/window.service';
import { TimeService } from '@core/services/time.service';
import { TooltipPosition } from '@angular/material/typings/tooltip';
@Component({
selector: 'tb-timewindow',
templateUrl: './timewindow.component.html',
styleUrls: ['./timewindow.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => TimewindowComponent),
multi: true
}
]
})
export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAccessor {
historyOnlyValue = false;
@Input()
set historyOnly(val) {
this.historyOnlyValue = true;
}
get historyOnly() {
return this.historyOnlyValue;
}
aggregationValue = false;
@Input()
set aggregation(val) {
this.aggregationValue = true;
}
get aggregation() {
return this.aggregationValue;
}
isToolbarValue = false;
@Input()
set isToolbar(val) {
this.isToolbarValue = true;
}
get isToolbar() {
return this.isToolbarValue;
}
asButtonValue = false;
@Input()
set asButton(val) {
this.asButtonValue = true;
}
get asButton() {
return this.asButtonValue;
}
@Input()
direction: 'left' | 'right' = 'left';
@Input()
tooltipPosition: TooltipPosition = 'above';
@Input() disabled: boolean;
@ViewChild('timewindowPanelOrigin', {static: false}) timewindowPanelOrigin: CdkOverlayOrigin;
innerValue: Timewindow;
private propagateChange = (_: any) => {};
constructor(private translate: TranslateService,
private timeService: TimeService,
private millisecondsToTimeStringPipe: MillisecondsToTimeStringPipe,
private datePipe: DatePipe,
private overlay: Overlay,
public viewContainerRef: ViewContainerRef,
public breakpointObserver: BreakpointObserver,
@Inject(DOCUMENT) private document: Document,
@Inject(WINDOW) private window: Window) {
}
ngOnInit(): void {
}
ngOnDestroy(): void {
}
openEditMode() {
if (this.disabled) {
return;
}
const isGtSm = this.breakpointObserver.isMatched(MediaBreakpoints['gt-sm']);
const position = this.overlay.position();
const config = new OverlayConfig({
panelClass: 'tb-timewindow-panel',
backdropClass: 'cdk-overlay-transparent-backdrop',
hasBackdrop: isGtSm,
});
if (isGtSm) {
config.minWidth = '417px';
config.maxHeight = '440px';
const panelHeight = 375;
const panelWidth = 417;
const el = this.timewindowPanelOrigin.elementRef.nativeElement;
const offset = el.getBoundingClientRect();
const scrollTop = this.window.pageYOffset || this.document.documentElement.scrollTop || this.document.body.scrollTop || 0;
const scrollLeft = this.window.pageXOffset || this.document.documentElement.scrollLeft || this.document.body.scrollLeft || 0;
const bottomY = offset.bottom - scrollTop;
const leftX = offset.left - scrollLeft;
let originX;
let originY;
let overlayX;
let overlayY;
const wHeight = this.document.documentElement.clientHeight;
const wWidth = this.document.documentElement.clientWidth;
if (bottomY + panelHeight > wHeight) {
originY = 'top';
overlayY = 'bottom';
} else {
originY = 'bottom';
overlayY = 'top';
}
if (leftX + panelWidth > wWidth) {
originX = 'end';
overlayX = 'end';
} else {
originX = 'start';
overlayX = 'start';
}
const connectedPosition: ConnectedPosition = {
originX,
originY,
overlayX,
overlayY
};
config.positionStrategy = position.flexibleConnectedTo(this.timewindowPanelOrigin.elementRef)
.withPositions([connectedPosition]);
} else {
config.minWidth = '100%';
config.minHeight = '100%';
config.positionStrategy = position.global().top('0%').left('0%')
.right('0%').bottom('0%');
}
const overlayRef = this.overlay.create(config);
overlayRef.backdropClick().subscribe(() => {
overlayRef.dispose();
});
const injector = this._createTimewindowPanelInjector(
overlayRef,
{
timewindow: this.innerValue.clone(),
historyOnly: this.historyOnly,
aggregation: this.aggregation
}
);
const componentRef = overlayRef.attach(new ComponentPortal(TimewindowPanelComponent, this.viewContainerRef, injector));
componentRef.onDestroy(() => {
if (componentRef.instance.result) {
this.innerValue = componentRef.instance.result;
this.updateDisplayValue();
this.notifyChanged();
}
});
}
private _createTimewindowPanelInjector(overlayRef: OverlayRef, data: TimewindowPanelData): PortalInjector {
const injectionTokens = new WeakMap<any, any>([
[TIMEWINDOW_PANEL_DATA, data],
[OverlayRef, overlayRef]
]);
return new PortalInjector(this.viewContainerRef.injector, injectionTokens);
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}
writeValue(obj: Timewindow): void {
this.innerValue = Timewindow.initModelFromDefaultTimewindow(obj, this.timeService);
this.updateDisplayValue();
}
notifyChanged() {
this.propagateChange(this.innerValue.cloneSelectedTimewindow());
}
updateDisplayValue() {
if (this.innerValue.selectedTab === TimewindowType.REALTIME && !this.historyOnly) {
this.innerValue.displayValue = this.translate.instant('timewindow.realtime') + ' - ' +
this.translate.instant('timewindow.last-prefix') + ' ' +
this.millisecondsToTimeStringPipe.transform(this.innerValue.realtime.timewindowMs);
} else {
this.innerValue.displayValue = !this.historyOnly ? (this.translate.instant('timewindow.history') + ' - ') : '';
if (this.innerValue.history.historyType === HistoryWindowType.LAST_INTERVAL) {
this.innerValue.displayValue += this.translate.instant('timewindow.last-prefix') + ' ' +
this.millisecondsToTimeStringPipe.transform(this.innerValue.history.timewindowMs);
} else {
const startString = this.datePipe.transform(this.innerValue.history.fixedTimewindow.startTimeMs, 'yyyy-MM-dd HH:mm:ss');
const endString = this.datePipe.transform(this.innerValue.history.fixedTimewindow.endTimeMs, 'yyyy-MM-dd HH:mm:ss');
this.innerValue.displayValue += this.translate.instant('timewindow.period', {startTime: startString, endTime: endString});
}
}
}
hideLabel() {
return this.isToolbar && !this.breakpointObserver.isMatched(MediaBreakpoints['gt-md']);
}
}

6
ui-ngx/src/app/shared/models/customer.model.ts

@ -23,3 +23,9 @@ export interface Customer extends ContactBased<CustomerId> {
title: string;
additionalInfo?: any;
}
export interface ShortCustomerInfo {
customerId: CustomerId;
title: string;
isPublic: boolean;
}

35
ui-ngx/src/app/shared/models/dashboard.models.ts

@ -0,0 +1,35 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {BaseData} from '@shared/models/base-data';
import {DashboardId} from '@shared/models/id/dashboard-id';
import {TenantId} from '@shared/models/id/tenant-id';
import {ShortCustomerInfo} from '@shared/models/customer.model';
export interface DashboardInfo extends BaseData<DashboardId> {
tenantId: TenantId;
title: string;
assignedCustomers: Array<ShortCustomerInfo>;
}
export interface DashboardConfiguration {
widgets: Array<any>;
// TODO:
}
export interface Dashboard extends DashboardInfo {
configuration: DashboardConfiguration;
}

113
ui-ngx/src/app/shared/models/datasource/entity-datasource.ts

@ -0,0 +1,113 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { EntityId } from '@shared/models/id/entity-id';
import { PageLink } from '@shared/models/page/page-link';
import { BehaviorSubject, Observable, of, ReplaySubject } from 'rxjs';
import { emptyPageData, PageData } from '@shared/models/page/page-data';
import { BaseData, HasId } from '@shared/models/base-data';
import { CollectionViewer, DataSource } from '@angular/cdk/typings/collections';
import { catchError, map, take, tap } from 'rxjs/operators';
import { SelectionModel } from '@angular/cdk/collections';
export type EntitiesFetchFunction<T extends BaseData<HasId>, P extends PageLink> = (pageLink: P) => Observable<PageData<T>>;
export class EntitiesDataSource<T extends BaseData<HasId>, P extends PageLink = PageLink> implements DataSource<T> {
private entitiesSubject = new BehaviorSubject<T[]>([]);
private pageDataSubject = new BehaviorSubject<PageData<T>>(emptyPageData<T>());
public pageData$ = this.pageDataSubject.asObservable();
public selection = new SelectionModel<T>(true, []);
public currentEntity: T = null;
constructor(private fetchFunction: EntitiesFetchFunction<T, P>) {}
connect(collectionViewer: CollectionViewer): Observable<T[] | ReadonlyArray<T>> {
return this.entitiesSubject.asObservable();
}
disconnect(collectionViewer: CollectionViewer): void {
this.entitiesSubject.complete();
this.pageDataSubject.complete();
}
loadEntities(pageLink: P): Observable<PageData<T>> {
const result = new ReplaySubject<PageData<T>>();
this.fetchFunction(pageLink).pipe(
tap(() => {
this.selection.clear();
}),
catchError(() => of(emptyPageData<T>())),
).subscribe(
(pageData) => {
this.entitiesSubject.next(pageData.data);
this.pageDataSubject.next(pageData);
result.next(pageData);
}
);
return result;
}
isAllSelected(): Observable<boolean> {
const numSelected = this.selection.selected.length;
return this.entitiesSubject.pipe(
map((entities) => numSelected === entities.length)
);
}
isEmpty(): Observable<boolean> {
return this.entitiesSubject.pipe(
map((entities) => !entities.length)
);
}
total(): Observable<number> {
return this.pageDataSubject.pipe(
map((pageData) => pageData.totalElements)
);
}
toggleCurrentEntity(entity: T): boolean {
if (this.currentEntity !== entity) {
this.currentEntity = entity;
return true;
} else {
return false;
}
}
isCurrentEntity(entity: T): boolean {
return (this.currentEntity && entity && this.currentEntity.id && entity.id) &&
(this.currentEntity.id.id === entity.id.id);
}
masterToggle() {
this.entitiesSubject.pipe(
tap((entities) => {
const numSelected = this.selection.selected.length;
if (numSelected === entities.length) {
this.selection.clear();
} else {
entities.forEach(row => this.selection.select(row));
}
}),
take(1)
).subscribe();
}
}

26
ui-ngx/src/app/shared/models/id/dashboard-id.ts

@ -0,0 +1,26 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { EntityId } from './entity-id';
import { EntityType } from '@shared/models/entity-type.models';
export class DashboardId implements EntityId {
entityType = EntityType.DASHBOARD;
id: string;
constructor(id: string) {
this.id = id;
}
}

17
ui-ngx/src/app/shared/models/settings.models.ts

@ -33,3 +33,20 @@ export interface MailServerSettings {
username: string;
password: string;
}
export interface GeneralSettings {
baseUrl: string;
}
export interface UserPasswordPolicy {
minimumLength: number;
minimumUppercaseLetters: number;
minimumLowercaseLetters: number;
minimumDigits: number;
minimumSpecialCharacters: number;
passwordExpirationPeriodDays: number;
}
export interface SecuritySettings {
passwordPolicy: UserPasswordPolicy;
}

27
ui-ngx/src/app/shared/pipe/enum-to-array.pipe.ts

@ -0,0 +1,27 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'enumToArray'
})
export class EnumToArrayPipe implements PipeTransform {
transform(data: object) {
const keys = Object.keys(data);
return keys.slice(keys.length / 2);
}
}

28
ui-ngx/src/app/shared/pipe/highlight.pipe.ts

@ -0,0 +1,28 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {Pipe, PipeTransform} from '@angular/core';
@Pipe({ name: 'highlight' })
export class HighlightPipe implements PipeTransform {
transform(text: string, search): string {
const pattern = search
.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&');
const regex = new RegExp('^' + pattern, 'i');
return search ? text.replace(regex, match => `<b>${match}</b>`) : text;
}
}

58
ui-ngx/src/app/shared/pipe/milliseconds-to-time-string.pipe.ts

@ -0,0 +1,58 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Pipe, PipeTransform } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
@Pipe({
name: 'milliSecondsToTimeString'
})
export class MillisecondsToTimeStringPipe implements PipeTransform {
constructor(private translate: TranslateService) {
}
transform(millseconds: number, args?: any): string {
let seconds = Math.floor(millseconds / 1000);
const days = Math.floor(seconds / 86400);
let hours = Math.floor((seconds % 86400) / 3600);
let minutes = Math.floor(((seconds % 86400) % 3600) / 60);
seconds = seconds % 60;
let timeString = '';
if (days > 0) {
timeString += this.translate.instant('timewindow.days', {days});
}
if (hours > 0) {
if (timeString.length === 0 && hours === 1) {
hours = 0;
}
timeString += this.translate.instant('timewindow.hours', {hours});
}
if (minutes > 0) {
if (timeString.length === 0 && minutes === 1) {
minutes = 0;
}
timeString += this.translate.instant('timewindow.minutes', {minutes});
}
if (seconds > 0) {
if (timeString.length === 0 && seconds === 1) {
seconds = 0;
}
timeString += this.translate.instant('timewindow.seconds', {seconds});
}
return timeString;
}
}

84
ui-ngx/src/app/shared/shared.module.ts

@ -58,39 +58,41 @@ import { NospacePipe } from './pipe/nospace.pipe';
import { TranslateModule } from '@ngx-translate/core';
import { TbCheckboxComponent } from '@shared/components/tb-checkbox.component';
import { HelpComponent } from '@shared/components/help.component';
// import { EntitiesTableComponent } from '@shared/components/entity/entities-table.component';
// import { AddEntityDialogComponent } from '@shared/components/entity/add-entity-dialog.component';
// import { DetailsPanelComponent } from '@shared/components/details-panel.component';
// import { EntityDetailsPanelComponent } from '@shared/components/entity/entity-details-panel.component';
import { EntitiesTableComponent } from '@shared/components/entity/entities-table.component';
import { AddEntityDialogComponent } from '@shared/components/entity/add-entity-dialog.component';
import { DetailsPanelComponent } from '@shared/components/details-panel.component';
import { EntityDetailsPanelComponent } from '@shared/components/entity/entity-details-panel.component';
import { TbAnchorComponent } from '@shared/components/tb-anchor.component';
// import { ContactComponent } from '@shared/components/contact.component';
import { ContactComponent } from '@shared/components/contact.component';
// import { AuditLogDetailsDialogComponent } from '@shared/components/audit-log/audit-log-details-dialog.component';
// import { AuditLogTableComponent } from '@shared/components/audit-log/audit-log-table.component';
// import { MillisecondsToTimeStringPipe } from '@shared/pipe/milliseconds-to-time-string.pipe';
// import { TimewindowComponent } from '@shared/components/time/timewindow.component';
import { MillisecondsToTimeStringPipe } from '@shared/pipe/milliseconds-to-time-string.pipe';
import { TimewindowComponent } from '@shared/components/time/timewindow.component';
import { OverlayModule } from '@angular/cdk/overlay';
// import { TimewindowPanelComponent } from '@shared/components/time/timewindow-panel.component';
// import { TimeintervalComponent } from '@shared/components/time/timeinterval.component';
// import { DatetimePeriodComponent } from '@shared/components/time/datetime-period.component';
// import { EnumToArrayPipe } from '@shared/pipe/enum-to-array.pipe';
import { TimewindowPanelComponent } from '@shared/components/time/timewindow-panel.component';
import { TimeintervalComponent } from '@shared/components/time/timeinterval.component';
import { DatetimePeriodComponent } from '@shared/components/time/datetime-period.component';
import { EnumToArrayPipe } from '@shared/pipe/enum-to-array.pipe';
import { ClipboardModule } from 'ngx-clipboard';
// import { ValueInputComponent } from '@shared/components/value-input.component';
// import { IntervalCountPipe } from '@shared/pipe/interval-count.pipe';
import { FullscreenDirective } from '@shared/components/fullscreen.directive';
import { HighlightPipe } from '@shared/pipe/highlight.pipe';
import {DashboardAutocompleteComponent} from '@shared/components/dashboard-autocomplete.component';
@NgModule({
providers: [
DatePipe,
// MillisecondsToTimeStringPipe,
// EnumToArrayPipe,
MillisecondsToTimeStringPipe,
EnumToArrayPipe,
HighlightPipe
// IntervalCountPipe,
],
entryComponents: [
TbSnackBarComponent,
TbAnchorComponent,
// AddEntityDialogComponent,
AddEntityDialogComponent,
// AuditLogDetailsDialogComponent,
// TimewindowPanelComponent,
TimewindowPanelComponent,
],
declarations: [
FooterComponent,
@ -103,22 +105,23 @@ import { FullscreenDirective } from '@shared/components/fullscreen.directive';
TbSnackBarComponent,
BreadcrumbComponent,
UserMenuComponent,
// EntitiesTableComponent,
// AddEntityDialogComponent,
// DetailsPanelComponent,
// EntityDetailsPanelComponent,
// ContactComponent,
EntitiesTableComponent,
AddEntityDialogComponent,
DetailsPanelComponent,
EntityDetailsPanelComponent,
ContactComponent,
// AuditLogTableComponent,
// AuditLogDetailsDialogComponent,
// TimewindowComponent,
// TimewindowPanelComponent,
// TimeintervalComponent,
// DatetimePeriodComponent,
TimewindowComponent,
TimewindowPanelComponent,
TimeintervalComponent,
DatetimePeriodComponent,
// ValueInputComponent,
DashboardAutocompleteComponent,
NospacePipe,
// MillisecondsToTimeStringPipe,
// EnumToArrayPipe,
// IntervalCountPipe
MillisecondsToTimeStringPipe,
EnumToArrayPipe,
HighlightPipe
],
imports: [
CommonModule,
@ -169,16 +172,17 @@ import { FullscreenDirective } from '@shared/components/fullscreen.directive';
TbCheckboxComponent,
BreadcrumbComponent,
UserMenuComponent,
// EntitiesTableComponent,
// AddEntityDialogComponent,
// DetailsPanelComponent,
// EntityDetailsPanelComponent,
// ContactComponent,
EntitiesTableComponent,
AddEntityDialogComponent,
DetailsPanelComponent,
EntityDetailsPanelComponent,
ContactComponent,
// AuditLogTableComponent,
// TimewindowComponent,
// TimewindowPanelComponent,
// TimeintervalComponent,
// DatetimePeriodComponent,
TimewindowComponent,
TimewindowPanelComponent,
TimeintervalComponent,
DatetimePeriodComponent,
DashboardAutocompleteComponent,
// ValueInputComponent,
MatButtonModule,
MatCheckboxModule,
@ -215,9 +219,9 @@ import { FullscreenDirective } from '@shared/components/fullscreen.directive';
ReactiveFormsModule,
OverlayModule,
NospacePipe,
// MillisecondsToTimeStringPipe,
// EnumToArrayPipe,
// IntervalCountPipe,
MillisecondsToTimeStringPipe,
EnumToArrayPipe,
HighlightPipe,
TranslateModule
]
})

3
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -590,6 +590,7 @@
"add-datasource-prompt": "Please add datasource"
},
"details": {
"details": "Details",
"edit-mode": "Edit mode",
"toggle-edit-mode": "Toggle edit mode"
},
@ -1378,6 +1379,7 @@
"delete-tenants-title": "Are you sure you want to delete { count, plural, 1 {1 tenant} other {# tenants} }?",
"delete-tenants-action-title": "Delete { count, plural, 1 {1 tenant} other {# tenants} }",
"delete-tenants-text": "Be careful, after the confirmation all selected tenants will be removed and all related data will become unrecoverable.",
"created-time": "Created time",
"title": "Title",
"title-required": "Title is required.",
"description": "Description",
@ -1437,6 +1439,7 @@
"delete-users-text": "Be careful, after the confirmation all selected users will be removed and all related data will become unrecoverable.",
"activation-email-sent-message": "Activation email was successfully sent!",
"resend-activation": "Resend activation",
"created-time": "Created time",
"email": "Email",
"email-required": "Email is required.",
"invalid-email-format": "Invalid email format.",

2
ui-ngx/src/assets/locale/locale.constant-es_ES.json

@ -152,7 +152,7 @@
"aknowledge-alarm-title": "Reconocer alarma",
"aknowledge-alarm-text": "¿Está seguro que quiere reconocer la alarma?",
"clear-alarms-title": "Quitar { count, plural, 1 {1 alarma} other {# alarmas} }",
"clear-alarms-text": "¿Está seguro de que desea quitar { count, plural, 1 {1 alarma} other {# alarmas}?",
"clear-alarms-text": "¿Está seguro de que desea quitar { count, plural, 1 {1 alarma} other {# alarmas} }?",
"clear-alarm-title": "Quitar alarma",
"clear-alarm-text": "¿Está seguro que quiere quitar la alarma?",
"alarm-status-filter": "Filtro de estado de alarma"

130
ui-ngx/src/assets/locale/locale.constant-fr_FR.json

@ -88,16 +88,16 @@
"alarm": {
"ack-time": "Heure d'acquittement",
"acknowledge": "Acquitter",
"aknowledge-alarms-text": "Etes-vous sûr de vouloir acquitter {count, plural, 1 {1 alarme} other {# alarmes}}?",
"aknowledge-alarms-title": "Acquitter {count, plural, 1 {1 alarme} other {# alarmes}}",
"aknowledge-alarms-text": "Etes-vous sûr de vouloir acquitter { count, plural, 1 {1 alarme} other {# alarmes} }?",
"aknowledge-alarms-title": "Acquitter { count, plural, 1 {1 alarme} other {# alarmes} }",
"alarm": "Alarme",
"alarm-details": "Détails de l'alarme",
"alarm-required": "Une alarme est requise",
"alarm-status": "Etat d'alarme",
"alarms": "Alarmes",
"clear": "Effacer",
"clear-alarms-text": "Êtes-vous sûr de vouloir effacer {count, plural, 1 {1 alarme} other {# alarmes}}?",
"clear-alarms-title": "Effacer {count, plural, 1 {1 alarme} other {# alarmes}}",
"clear-alarms-text": "Êtes-vous sûr de vouloir effacer { count, plural, 1 {1 alarme} other {# alarmes} }?",
"clear-alarms-title": "Effacer { count, plural, 1 {1 alarme} other {# alarmes} }",
"clear-time": "Heure d'éffacement",
"created-time": "Heure de création",
"details": "Détails",
@ -125,7 +125,7 @@
"UNACK": "non acquittée"
},
"select-alarm": "Sélectionnez une alarme",
"selected-alarms": "{count, plural, 1 {1 alarme} other {# alarmes}} sélectionnées",
"selected-alarms": "{ count, plural, 1 {1 alarme} other {# alarmes} } sélectionnées",
"severity": "Gravitée",
"severity-critical": "Critique",
"severity-indeterminate": "indéterminée",
@ -192,7 +192,7 @@
"assign-asset-to-customer": "Attribuer des Assets au client",
"assign-asset-to-customer-text": "Veuillez sélectionner les Assets à attribuer au client",
"assign-assets": "Attribuer des Assets",
"assign-assets-text": "Attribuer {count, plural, 1 {1 asset} other {# assets}} au client",
"assign-assets-text": "Attribuer { count, plural, 1 {1 asset} other {# assets} } au client",
"assign-new-asset": "Attribuer un nouvel Asset",
"assign-to-customer": "Attribuer au client",
"assign-to-customer-text": "Veuillez sélectionner le client pour attribuer le ou les Assets",
@ -202,9 +202,9 @@
"delete-asset-text": "Faites attention, après la confirmation, l'Asset et toutes les données associées deviendront irrécupérables.",
"delete-asset-title": "Êtes-vous sûr de vouloir supprimer l'Asset '{{assetName}}'?",
"delete-assets": "Supprimer des Assets",
"delete-assets-action-title": "Supprimer {count, plural, 1 {1 asset} other {# assets}}",
"delete-assets-action-title": "Supprimer { count, plural, 1 {1 asset} other {# assets} }",
"delete-assets-text": "Attention, après la confirmation, tous les Assets sélectionnés seront supprimés et toutes les données associées deviendront irrécupérables.",
"delete-assets-title": "Etes-vous sûr de vouloir supprimer {count, plural, 1 {1 asset} other {# assets}}?",
"delete-assets-title": "Etes-vous sûr de vouloir supprimer { count, plural, 1 {1 asset} other {# assets} }?",
"description": "Description",
"details": "Détails",
"enter-asset-type": "Entrez le type d'Asset",
@ -232,9 +232,9 @@
"unassign-asset-text": "Après la confirmation, l'Asset sera non attribué et ne sera pas accessible au client.",
"unassign-asset-title": "Êtes-vous sûr de vouloir retirer l'attribution de l'Asset '{{assetName}}'?",
"unassign-assets": "Retirer les Assets",
"unassign-assets-action-title": "Retirer {count, plural, 1 {1 asset} other {# assets}} du client",
"unassign-assets-action-title": "Retirer { count, plural, 1 {1 asset} other {# assets} } du client",
"unassign-assets-text": "Après la confirmation, tous les Assets sélectionnés ne seront pas attribués et ne seront pas accessibles au client.",
"unassign-assets-title": "Êtes-vous sûr de vouloir retirer l'attribution de {count, plural, 1 {1 asset} other {# assets}}?",
"unassign-assets-title": "Êtes-vous sûr de vouloir retirer l'attribution de { count, plural, 1 {1 asset} other {# assets} }?",
"unassign-from-customer": "Retirer du client",
"view-assets": "Afficher les Assets"
},
@ -246,7 +246,7 @@
"attributes-scope": "Etendue des attributs d'entité",
"delete-attributes": "Supprimer les attributs",
"delete-attributes-text": "Attention, après la confirmation, tous les attributs sélectionnés seront supprimés.",
"delete-attributes-title": "Êtes-vous sûr de vouloir supprimer {count, plural, 1 {1 attribut} other {# attributs}}?",
"delete-attributes-title": "Êtes-vous sûr de vouloir supprimer { count, plural, 1 {1 attribut} other {# attributs} }?",
"enter-attribute-value": "Entrez la valeur de l'attribut",
"key": "Clé",
"key-required": "La Clé d'attribut est requise.",
@ -258,8 +258,8 @@
"scope-latest-telemetry": "Dernière télémétrie",
"scope-server": "Attributs du serveur",
"scope-shared": "Attributs partagés",
"selected-attributes": "{count, plural, 1 {1 attribut} other {# attributs}} sélectionnés",
"selected-telemetry": "{count, plural, 1 {1 unité de télémétrie} other {# unités de télémétrie}} sélectionnées",
"selected-attributes": "{ count, plural, 1 {1 attribut} other {# attributs} } sélectionnés",
"selected-telemetry": "{ count, plural, 1 {1 unité de télémétrie} other {# unités de télémétrie} } sélectionnées",
"show-on-widget": "Afficher sur le widget",
"value": "Valeur",
"value-required": "La valeur d'attribut est obligatoire.",
@ -358,9 +358,9 @@
"delete": "Supprimer le client",
"delete-customer-text": "Faites attention, après la confirmation, le client et toutes les données associées deviendront irrécupérables.",
"delete-customer-title": "Êtes-vous sûr de vouloir supprimer le client '{{customerTitle}}'?",
"delete-customers-action-title": "Supprimer {count, plural, 1 {1 client} other {# clients}}",
"delete-customers-action-title": "Supprimer { count, plural, 1 {1 client} other {# clients} }",
"delete-customers-text": "Faites attention, après la confirmation, tous les clients sélectionnés seront supprimés et toutes les données associées deviendront irrécupérables.",
"delete-customers-title": "Êtes-vous sûr de vouloir supprimer {count, plural, 1 {1 client} other {# clients}}?",
"delete-customers-title": "Êtes-vous sûr de vouloir supprimer { count, plural, 1 {1 client} other {# clients} }?",
"description": "Description",
"details": "Détails",
"devices": "Dispositifs du client",
@ -397,7 +397,7 @@
"assign-dashboard-to-customer": "Attribuer des tableaux de bord au client",
"assign-dashboard-to-customer-text": "Veuillez sélectionner les tableaux de bord à affecter au client",
"assign-dashboards": "Attribuer des tableaux de bord",
"assign-dashboards-text": "Attribuer {count, plural, 1 {1 tableau de bord} other {# tableaux de bord}} aux clients",
"assign-dashboards-text": "Attribuer { count, plural, 1 {1 tableau de bord} other {# tableaux de bord} } aux clients",
"assign-new-dashboard": "Attribuer un nouveau tableau de bord",
"assign-to-customer": "Attribuer au client",
"assign-to-customer-text": "Veuillez sélectionner le client pour attribuer le ou les tableaux de bord",
@ -428,9 +428,9 @@
"delete-dashboard-text": "Faites attention, après la confirmation, le tableau de bord et toutes les données associées deviendront irrécupérables.",
"delete-dashboard-title": "Êtes-vous sûr de vouloir supprimer le tableau de bord '{{dashboardTitle}}'?",
"delete-dashboards": "Supprimer les tableaux de bord",
"delete-dashboards-action-title": "Supprimer {count, plural, 1 {1 tableau de bord} other {# tableaux de bord}}",
"delete-dashboards-action-title": "Supprimer {count, plural, 1 {1 tableau de bord} other {# tableaux de bord} }",
"delete-dashboards-text": "Attention, après la confirmation, tous les tableaux de bord sélectionnés seront supprimés et toutes les données associées deviendront irrécupérables.",
"delete-dashboards-title": "Voulez-vous vraiment supprimer {count, plural, 1 {1 tableau de bord} other {# tableaux de bord}}?",
"delete-dashboards-title": "Voulez-vous vraiment supprimer {count, plural, 1 {1 tableau de bord} other {# tableaux de bord} }?",
"delete-state": "Supprimer l'état du tableau de bord",
"delete-state-text": "Etes-vous sûr de vouloir supprimer l'état du tableau de bord avec le nom '{{stateName}}'?",
"delete-state-title": "Supprimer l'état du tableau de bord",
@ -493,7 +493,7 @@
"select-state": "Sélectionnez l'état cible",
"select-widget-subtitle": "Liste des types de widgets disponibles",
"select-widget-title": "Sélectionner un widget",
"selected-states": "{count, plural, 1 {1 état du tableau de bord} other {# états du tableau de bord}} sélectionnés",
"selected-states": "{count, plural, 1 {1 état du tableau de bord} other {# états du tableau de bord} } sélectionnés",
"set-background": "Définir l'arrière-plan",
"settings": "Paramètres",
"show-details": "Afficher les détails",
@ -515,10 +515,10 @@
"unassign-dashboard-text": "Après la confirmation, le tableau de bord ne sera pas attribué et ne sera pas accessible au client.",
"unassign-dashboard-title": "Êtes-vous sûr de vouloir annuler l'affectation du tableau de bord '{{dashboardTitle}}'?",
"unassign-dashboards": "Retirer les tableaux de bord",
"unassign-dashboards-action-text": "Annuler l'affectation {count, plural, 1 {1 tableau de bord} other {# tableaux de bord}} des clients",
"unassign-dashboards-action-title": "Annuler l'affectation {count, plural, 1 {1 tableau de bord} other {# tableaux de bord}} du client",
"unassign-dashboards-action-text": "Annuler l'affectation {count, plural, 1 {1 tableau de bord} other {# tableaux de bord} } des clients",
"unassign-dashboards-action-title": "Annuler l'affectation {count, plural, 1 {1 tableau de bord} other {# tableaux de bord} } du client",
"unassign-dashboards-text": "Après la confirmation, tous les tableaux de bord sélectionnés ne seront pas attribués et ne seront pas accessibles au client.",
"unassign-dashboards-title": "Etes-vous sûr de vouloir annuler l'affectation {count, plural, 1 {1 tableau de bord} other {# tableaux de bord}}?",
"unassign-dashboards-title": "Etes-vous sûr de vouloir annuler l'affectation {count, plural, 1 {1 tableau de bord} other {# tableaux de bord} }?",
"unassign-from-customer": "Retirer du client",
"unassign-from-customers": "Retirer les tableaux de bord des clients",
"unassign-from-customers-text": "Veuillez sélectionner les clients à annuler l'affectation du ou des tableaux de bord",
@ -541,8 +541,8 @@
"function-types": "Types de fonctions",
"function-types-required": "Les types de fonctions sont obligatoires",
"label": "Label",
"maximum-function-types": "Maximum {count, plural, 1 {1 type de fonction est autorisé.} other {# types de fonctions sont autorisés}}",
"maximum-timeseries-or-attributes": "Maximum {count, plural, 1 {1 timeseries / attribut est autorisé.} other {# timeseries / attributs sont autorisés}}",
"maximum-function-types": "Maximum {count, plural, 1 {1 type de fonction est autorisé.} other {# types de fonctions sont autorisés} }",
"maximum-timeseries-or-attributes": "Maximum {count, plural, 1 {1 timeseries / attribut est autorisé.} other {# timeseries / attributs sont autorisés} }",
"settings": "Paramètres",
"timeseries": "Timeseries",
"timeseries-or-attributes-required": "Les timeseries / attributs d'entité sont obligatoires.",
@ -580,7 +580,7 @@
"assign-device-to-customer": "Affecter des dispositifs au client",
"assign-device-to-customer-text": "Veuillez sélectionner les dispositif à affecter au client",
"assign-devices": "Attribuer des dispositifs",
"assign-devices-text": "Attribuer {count, plural, 1 {1 dispositif} other {# dispositifs}} au client",
"assign-devices-text": "Attribuer {count, plural, 1 {1 dispositif} other {# dispositifs} } au client",
"assign-new-device": "Attribuer un nouveau dispositif",
"assign-to-customer": "Attribuer au client",
"assign-to-customer-text": "Veuillez sélectionner le client pour attribuer le ou les dispositifs",
@ -596,9 +596,9 @@
"delete-device-text": "Faites attention, après la confirmation, le dispositif et toutes les données associées deviendront irrécupérables.",
"delete-device-title": "Êtes-vous sûr de vouloir supprimer le dispositif '{{deviceName}}'?",
"delete-devices": "Supprimer les dispositifs",
"delete-devices-action-title": "Supprimer {count, plural, 1 {1 dispositif} other {# dispositifs}}",
"delete-devices-action-title": "Supprimer {count, plural, 1 {1 dispositif} other {# dispositifs} }",
"delete-devices-text": "Faites attention, après la confirmation, tous les dispositifs sélectionnés seront supprimés et toutes les données associées deviendront irrécupérables.",
"delete-devices-title": "Êtes-vous sûr de vouloir supprimer {count, plural, 1 {1 dispositif} other {# dispositifs}}?",
"delete-devices-title": "Êtes-vous sûr de vouloir supprimer {count, plural, 1 {1 dispositif} other {# dispositifs} }?",
"description": "Description",
"details": "Détails",
"device": "Dispositif",
@ -653,9 +653,9 @@
"unassign-device-text": "Après la confirmation, le dispositif ne sera pas attribué et ne sera pas accessible au client.",
"unassign-device-title": "Êtes-vous sûr de vouloir annuler l'affection du dispositif {{deviceName}} '?",
"unassign-devices": "Annuler l'affectation des dispositifs",
"unassign-devices-action-title": "Annuler l'affectation de {count, plural, 1 {1 dispositif} other {#dispositifs}} du client",
"unassign-devices-action-title": "Annuler l'affectation de {count, plural, 1 {1 dispositif} other {#dispositifs} } du client",
"unassign-devices-text": "Après la confirmation, tous les dispositifs sélectionnés ne seront pas attribues et ne seront pas accessibles par le client.",
"unassign-devices-title": "Voulez-vous vraiment annuler l'affectation de {count, plural, 1 {1 dispositif} other {# dispositifs}}?",
"unassign-devices-title": "Voulez-vous vraiment annuler l'affectation de {count, plural, 1 {1 dispositif} other {# dispositifs} }?",
"unassign-from-customer": "Retirer du client",
"use-device-name-filter": "Utiliser le filtre",
"view-credentials": "Afficher les informations d'identification",
@ -696,17 +696,17 @@
"entity-types": "Types d'entité",
"key": "Clé",
"key-name": "Nom de la clé",
"list-of-alarms": "{count, plural, 1 {Une alarme} other {Liste de # alarmes}}",
"list-of-assets": "{count, plural, 1 {Un Asset} other {Liste de # Assets}}",
"list-of-customers": "{count, plural, 1 {Un client} other {Liste de # clients}}",
"list-of-dashboards": "{count, plural, 1 {Un tableau de bord} other {Liste de # tableaux de bord}}",
"list-of-devices": "{count, plural, 1 {Un dispositif} other {Liste de # dispositifs}}",
"list-of-plugins": "{count, plural, 1 {Un plugin} other {Liste de # plugins}}",
"list-of-rulechains": "{count, plural, 1 {Une chaîne de règles} other {Liste de # chaînes de règles}}",
"list-of-rulenodes": "{count, plural, 1 {Un noeud de règles} other {Liste de # noeuds de règles}}",
"list-of-rules": "{count, plural, 1 {Une règle} other {Liste de # règles}}",
"list-of-tenants": "{count, plural, 1 {Un tenant} other {Liste de # tenants}}",
"list-of-users": "{count, plural, 1 {Un utilisateur} other {Liste de # utilisateurs}}",
"list-of-alarms": "{count, plural, 1 {Une alarme} other {Liste de # alarmes} }",
"list-of-assets": "{count, plural, 1 {Un Asset} other {Liste de # Assets} }",
"list-of-customers": "{count, plural, 1 {Un client} other {Liste de # clients} }",
"list-of-dashboards": "{count, plural, 1 {Un tableau de bord} other {Liste de # tableaux de bord} }",
"list-of-devices": "{count, plural, 1 {Un dispositif} other {Liste de # dispositifs} }",
"list-of-plugins": "{count, plural, 1 {Un plugin} other {Liste de # plugins} }",
"list-of-rulechains": "{count, plural, 1 {Une chaîne de règles} other {Liste de # chaînes de règles} }",
"list-of-rulenodes": "{count, plural, 1 {Un noeud de règles} other {Liste de # noeuds de règles} }",
"list-of-rules": "{count, plural, 1 {Une règle} other {Liste de # règles} }",
"list-of-tenants": "{count, plural, 1 {Un tenant} other {Liste de # tenants} }",
"list-of-users": "{count, plural, 1 {Un utilisateur} other {Liste de # utilisateurs} }",
"missing-entity-filter-error": "Le filtre est manquant pour l'alias '{{alias}}'.",
"name-starts-with": "Nom commence par",
"no-alias-matching": "'{{alias}}' introuvable.",
@ -724,7 +724,7 @@
"rulenode-name-starts-with": "Les noeuds de règles dont le nom commence par '{{prefix}}'",
"search": "Recherche d'entités",
"select-entities": "Sélectionner des entités",
"selected-entities": "{count, plural, 1 {1 entité} other {# entités}} sélectionnées",
"selected-entities": "{count, plural, 1 {1 entité} other {# entités} } sélectionnées",
"tenant-name-starts-with": "Les Tenant dont le nom commence par '{{prefix}}'",
"type": "Type",
"type-alarm": "Alarme",
@ -832,7 +832,7 @@
"delete-extension-text": "Attention, après la confirmation, l'extension et toutes les données associées deviendront irrécupérables.",
"delete-extension-title": "Êtes-vous sûr de vouloir supprimer l'extension '{{extensionId}}'?",
"delete-extensions-text": "Attention, après la confirmation, toutes les extensions sélectionnées seront supprimées.",
"delete-extensions-title": "Êtes-vous sûr de vouloir supprimer {count, plural, 1 {1 extension} other {# extensions}}?",
"delete-extensions-title": "Êtes-vous sûr de vouloir supprimer {count, plural, 1 {1 extension} other {# extensions} }?",
"device-name-expression": "expression du nom du dispositif",
"device-name-filter": "Filtre de nom de dispositif",
"device-type-expression": "expression de type de dispositif",
@ -918,7 +918,7 @@
"response-timeout": "Délai de réponse en millisecondes",
"response-topic-expression": "Expression du topic de la réponse",
"retry-interval": "Intervalle de nouvelle tentative en millisecondes",
"selected-extensions": "{count, plural, 1 {1 extension} other {# extensions}} sélectionné",
"selected-extensions": "{count, plural, 1 {1 extension} other {# extensions} } sélectionné",
"server-side-rpc": "RPC côté serveur",
"ssl": "Ssl",
"sync": {
@ -960,9 +960,9 @@
"delete-item-text": "Faites attention, après la confirmation, cet élément et toutes les données associées deviendront irrécupérables.",
"delete-item-title": "Êtes-vous sûr de vouloir supprimer cet élément?",
"delete-items": "Supprimer les éléments",
"delete-items-action-title": "Supprimer {count, plural, 1 {1 élément} other {# éléments}}",
"delete-items-action-title": "Supprimer {count, plural, 1 {1 élément} other {# éléments} }",
"delete-items-text": "Attention, après la confirmation, tous les éléments sélectionnés seront supprimés et toutes les données associées deviendront irrécupérables.",
"delete-items-title": "Etes-vous sûr de vouloir supprimer {count, plural, 1 {1 élément} other {# éléments}}?",
"delete-items-title": "Etes-vous sûr de vouloir supprimer {count, plural, 1 {1 élément} other {# éléments} }?",
"item-details": "Détails de l'élément",
"no-items-text": "Aucun élément trouvé",
"scroll-to-top": "Défiler vers le haut"
@ -1080,11 +1080,11 @@
"delete-from-relation-text": "Attention, après la confirmation, l'entité actuelle ne sera pas liée à l'entité '{{entityName}}'.",
"delete-from-relation-title": "Etes-vous sûr de vouloir supprimer la relation de l'entité '{{entityName}}'?",
"delete-from-relations-text": "Attention, après la confirmation, toutes les relations sélectionnées seront supprimées et l'entité actuelle ne sera pas liée aux entités correspondantes.",
"delete-from-relations-title": "Êtes-vous sûr de vouloir supprimer {count, plural, 1 {1 relation} other {# relations}}?",
"delete-from-relations-title": "Êtes-vous sûr de vouloir supprimer {count, plural, 1 {1 relation} other {# relations} }?",
"delete-to-relation-text": "Attention, après la confirmation, l'entité '{{entityName}} ne sera plus liée à l'entité actuelle.",
"delete-to-relation-title": "Êtes-vous sûr de vouloir supprimer la relation avec l'entité '{{entityName}}'?",
"delete-to-relations-text": "Attention, après la confirmation, toutes les relations sélectionnées seront supprimées et les entités correspondantes ne seront pas liées à l'entité en cours.",
"delete-to-relations-title": "Etes-vous sûr de vouloir supprimer {count, plural, 1 {1 relation} other {# relations}}?",
"delete-to-relations-title": "Etes-vous sûr de vouloir supprimer {count, plural, 1 {1 relation} other {# relations} }?",
"direction": "Sens",
"direction-type": {
"FROM": "de",
@ -1105,7 +1105,7 @@
"FROM": "De",
"TO": "À"
},
"selected-relations": "{count, plural, 1 {1 relation} other {# relations}} sélectionné",
"selected-relations": "{count, plural, 1 {1 relation} other {# relations} } sélectionné",
"to-entity": "À l'entité",
"to-entity-name": "vers le nom de l'entité",
"to-entity-type": "Vers le type d'entité",
@ -1121,9 +1121,9 @@
"delete": "Supprimer la chaîne de règles",
"delete-rulechain-text": "Attention, après la confirmation, la chaîne de règles et toutes les données associées deviendront irrécupérables.",
"delete-rulechain-title": "Voulez-vous vraiment supprimer la chaîne de règles '{{ruleChainName}}'?",
"delete-rulechains-action-title": "Supprimer {count, plural, 1 {1 chaîne de règles} other {# chaînes de règles}}",
"delete-rulechains-action-title": "Supprimer {count, plural, 1 {1 chaîne de règles} other {# chaînes de règles} }",
"delete-rulechains-text": "Attention, après la confirmation, toutes les chaînes de règles sélectionnées seront supprimées et toutes les données associées deviendront irrécupérables.",
"delete-rulechains-title": "Êtes-vous sûr de vouloir supprimer {count, plural, 1 {1 chaîne de règles} other {# chaînes de règles}}?",
"delete-rulechains-title": "Êtes-vous sûr de vouloir supprimer {count, plural, 1 {1 chaîne de règles} other {# chaînes de règles} }?",
"description": "Description",
"details": "Détails",
"events": "Evénements",
@ -1220,9 +1220,9 @@
"delete": "Supprimer le Tenant",
"delete-tenant-text": "Attention, après la confirmation, le Tenant et toutes les données associées deviendront irrécupérables.",
"delete-tenant-title": "Etes-vous sûr de vouloir supprimer le tenant '{{tenantTitle}}'?",
"delete-tenants-action-title": "Supprimer {count, plural, 1 {1 tenant} other {# tenants}}",
"delete-tenants-action-title": "Supprimer {count, plural, 1 {1 tenant} other {# tenants} }",
"delete-tenants-text": "Attention, après la confirmation, tous les Tenants sélectionnés seront supprimés et toutes les données associées deviendront irrécupérables.",
"delete-tenants-title": "Etes-vous sûr de vouloir supprimer {count, plural, 1 {1 tenant} other {# tenants}}?",
"delete-tenants-title": "Etes-vous sûr de vouloir supprimer {count, plural, 1 {1 tenant} other {# tenants} }?",
"description": "Description",
"details": "Détails",
"events": "Événements",
@ -1242,26 +1242,26 @@
"timeinterval": {
"advanced": "Avancé",
"days": "Jours",
"days-interval": "{days, plural, 1 {1 jour} other {# jours}}",
"days-interval": "{days, plural, 1 {1 jour} other {# jours} }",
"hours": "Heures",
"hours-interval": "{hours, plural, 1 {1 heure} other {# heures}}",
"hours-interval": "{hours, plural, 1 {1 heure} other {# heures} }",
"minutes": "Minutes",
"minutes-interval": "{minutes, plural, 1 {1 minute} other {# minutes}}",
"minutes-interval": "{minutes, plural, 1 {1 minute} other {# minutes} }",
"seconds": "Secondes",
"seconds-interval": "{seconds, plural, 1 {1 seconde} other {# secondes}}"
"seconds-interval": "{seconds, plural, 1 {1 seconde} other {# secondes} }"
},
"timewindow": {
"date-range": "Plage de dates",
"days": "{days, plural, 1 {jour} other {# jours}}",
"days": "{days, plural, 1 {jour} other {# jours} }",
"edit": "Modifier timewindow",
"history": "Historique",
"hours": "{hours, plural, 0 {heure} 1 {1 heure} other {# heures}}",
"hours": "{hours, plural, 0 {heure} 1 {1 heure} other {# heures} }",
"last": "Dernier",
"last-prefix": "dernier",
"minutes": "{minutes, plural, 0 {minute} 1 {1 minute} other {# minutes}}",
"minutes": "{minutes, plural, 0 {minute} 1 {1 minute} other {# minutes} }",
"period": "de {{startTime}} à {{endTime}}",
"realtime": "Temps réel",
"seconds": "{seconds, plural, 0 {second} 1 {1 second} other {# seconds}}",
"seconds": "{seconds, plural, 0 {second} 1 {1 second} other {# seconds} }",
"time-period": "Période"
},
"user": {
@ -1281,9 +1281,9 @@
"delete": "Supprimer l'utilisateur",
"delete-user-text": "Attention, après la confirmation, l'utilisateur et toutes les données associées deviendront irrécupérables.",
"delete-user-title": "Etes-vous sûr de vouloir supprimer l'utilisateur '{{userEmail}}'?",
"delete-users-action-title": "Supprimer {count, plural, 1 {1 utilisateur} other {# utilisateurs}}",
"delete-users-action-title": "Supprimer {count, plural, 1 {1 utilisateur} other {# utilisateurs} }",
"delete-users-text": "Attention, après la confirmation, tous les utilisateurs sélectionnés seront supprimés et toutes les données associées deviendront irrécupérables.",
"delete-users-title": "Etes-vous sûr de vouloir supprimer {count, plural, 1 {1 utilisateur} other {# utilisateurs}}?",
"delete-users-title": "Etes-vous sûr de vouloir supprimer {count, plural, 1 {1 utilisateur} other {# utilisateurs} }?",
"description": "Description",
"details": "Détails",
"display-activation-link": "Afficher le lien d'activation",
@ -1417,7 +1417,7 @@
"general-settings": "Paramètres généraux",
"height": "Hauteur",
"margin": "Marge",
"maximum-datasources": "Maximum {count, plural, 1 {1 datasource est autorisé.} other {# datasources sont autorisés}}",
"maximum-datasources": "Maximum {count, plural, 1 {1 datasource est autorisé.} other {# datasources sont autorisés} }",
"mobile-mode-settings": "Paramètres du mode mobile",
"order": "Ordre",
"padding": "Padding",
@ -1511,9 +1511,9 @@
"delete": "Supprimer le groupe de widgets",
"delete-widgets-bundle-text": "Attention, après la confirmation, le groupe de widgets et toutes les données associées deviendront irrécupérables.",
"delete-widgets-bundle-title": "Êtes-vous sûr de vouloir supprimer le groupe de widgets '{{widgetsBundleTitle}}'?",
"delete-widgets-bundles-action-title": "Supprimer {count, plural, 1 {1 groupe de widgets} other {# groupes de widgets}}",
"delete-widgets-bundles-action-title": "Supprimer {count, plural, 1 {1 groupe de widgets} other {# groupes de widgets} }",
"delete-widgets-bundles-text": "Attention, après la confirmation, tous les groupes de widgets sélectionnés seront supprimés et toutes les données associées deviendront irrécupérables.",
"delete-widgets-bundles-title": "Voulez-vous vraiment supprimer {count, plural, 1 {1 groupe de widgets} other {# groupes de widgets}}?",
"delete-widgets-bundles-title": "Voulez-vous vraiment supprimer {count, plural, 1 {1 groupe de widgets} other {# groupes de widgets} }?",
"details": "Détails",
"empty": "Le groupe de widgets est vide",
"export": "Exporter le groupe de widgets",

6
ui-ngx/src/assets/locale/locale.constant-ru_RU.json

@ -626,7 +626,7 @@
"delete-device-title": "Вы точно хотите удалить устройство '{{deviceName}}'?",
"delete-device-text": "Внимание, после подтверждения устройство и все связанные с ним данные будут безвозвратно утеряны.",
"delete-devices-title": "Вы точно хотите удалить { count, plural, one {1 устройство} few {# устройства} other {# устройств} }?",
"delete-devices-action-title": "Удалить { count, plural, one {1 устройство} few {# устройства} other {# устройств} } }",
"delete-devices-action-title": "Удалить { count, plural, one {1 устройство} few {# устройства} other {# устройств} }",
"delete-devices-text": "Внимание, после подтверждения выбранные устройства и все связанные с ними данные будут безвозвратно утеряны..",
"unassign-device-title": "Вы точно хотите отозвать устройство '{{deviceName}}'?",
"unassign-device-text": "После подтверждения устройство будет недоступно клиенту.",
@ -1064,7 +1064,7 @@
"delete-item-title": "Вы точно хотите удалить этот объект?",
"delete-item-text": "Внимание, после подтверждения объект и все связанные с ним данные будут безвозвратно утеряны.",
"delete-items-title": "Вы точно хотите удалить { count, plural, one {1 объект} few {# объекта} other {# объектов} }?",
"delete-items-action-title": "Удалить { count, plural, one {1 объект} few {# объекта} other {# объектов}",
"delete-items-action-title": "Удалить { count, plural, one {1 объект} few {# объекта} other {# объектов} }",
"delete-items-text": "Внимание, после подтверждения выбранные объекты и все связанные с ними данные будут безвозвратно утеряны.",
"add-item-text": "Добавить новый объект",
"no-items-text": "Объекты не найдены",
@ -1646,4 +1646,4 @@
"cs_CZ": "Чешский"
}
}
}
}

26
ui-ngx/src/assets/locale/locale.constant-tr_TR.json

@ -419,7 +419,7 @@
"assign-dashboards": "Kontrol panelleri ata",
"assign-new-dashboard": "Yeni kontrol paneli ata",
"assign-dashboards-text": "{ count, plural, 1 {1 kontrol panelini} other {# kontrol panelini} } kullanıcı grubuna ata",
"unassign-dashboards-action-text": "Müşterilerden atama { count, plural, 1 {1 gösterge tablosu} other {# panolar}}",
"unassign-dashboards-action-text": "Müşterilerden atama { count, plural, 1 {1 gösterge tablosu} other {# panolar} }",
"delete-dashboards": "Kontrol panellerini sil",
"unassign-dashboards": "Kontrol panellerinden atamayı kaldır",
"unassign-dashboards-action-title": "{ count, plural, 1 {1 kontrol panelinin} other {# kontrol panelinin} } atamaları kullanıcı grubundan kaldır",
@ -710,7 +710,7 @@
"asset-name-starts-with": "İsmi '{{prefix}}' ile başlayan varlıklar",
"type-entity-view": "Varlık Görünümü",
"type-entity-views": "Varlık Görünümleri",
"list-of-entity-views": "{ count, plural, 1 {Bir varlık görünümü} other {# varlık görüntüleme}} listesi",
"list-of-entity-views": "{ count, plural, 1 {Bir varlık görünümü} other {# varlık görüntüleme} } listesi",
"entity-view-name-starts-with": "Adı {{önek}} ile başlayan varlık görünümleri",
"type-rule": "Kural",
"type-rules": "Kurallar",
@ -742,11 +742,11 @@
"alarm-name-starts-with": "İsmi '{{prefix}}' ile başlayan alarmlar",
"type-rulechain": "Kural zinciri",
"type-rulechains": "Kural zincirleri",
"list-of-rulechains": "{ count, plural, 1 {Bir kural zinciri} other {# kural zincirinin listesi}}",
"list-of-rulechains": "{ count, plural, 1 {Bir kural zinciri} other {# kural zincirinin listesi} }",
"rulechain-name-starts-with": "İsimleri {{prefix}} ile başlayan kural zincirleri",
"type-rulenode": "Kural düğümü",
"type-rulenodes": "Kural düğümleri",
"list-of-rulenodes": "{ count, plural, 1 {Bir kural node} other {# kural düğümünün listesi}}",
"list-of-rulenodes": "{ count, plural, 1 {Bir kural node} other {# kural düğümünün listesi} }",
"rulenode-name-starts-with": "İsimleri '{{prefix}} ile başlayan kural düğümleri",
"type-current-customer": "Mevcut Müşteri",
"search": "Öğeleri ara",
@ -765,7 +765,7 @@
"aliases": "Varlık Görünümü takma adları",
"no-alias-matching": "'{{alias}} bulunamadı. ",
"no-aliases-found": "Takma ad bulunamadı",
"no-key-matching": "'{{anahtar bulunamadı.",
"no-key-matching": "'{{key}}' bulunamadı.",
"no-keys-found": "Anahtar bulunamadı.",
"create-new-alias": "Yeni bir tane oluştur!",
"create-new-key": "Yeni bir tane oluştur!",
@ -792,21 +792,21 @@
"add-entity-view-text": "Yeni varlık görünümü ekle",
"delete": "Varlık görünümünü sil",
"assign-entity-views": "Varlık görünümleri atama",
"assign-entity-views-text": "Müşteriye { count, plural, 1 {1 entityView} other {# entityViews}} atayın ",
"assign-entity-views-text": "Müşteriye { count, plural, 1 {1 entityView} other {# entityViews} } atayın ",
"delete-entity-views": "Varlık görünümlerini sil",
"unassign-from-customer": "Müşteriden atama",
"unassign-entity-views": "Varlık görünümlerini atama",
"unassign-entity-views-action-title": "Müşteriden atama { count, plural, 1 {1 entityView} other {# entityViews}}",
"unassign-entity-views-action-title": "Müşteriden atama { count, plural, 1 {1 entityView} other {# entityViews} }",
"assign-new-entity-view": "Yeni varlık görünümü atama",
"delete-entity-view-title": "Varlık görünümünü silmek istediğinizden emin misiniz?, {{entityViewName}} '? ",
"delete-entity-view-text": "Dikkatli olun, onaylandıktan sonra varlık görünümü ve ilgili tüm veriler kurtarılamayacak.",
"delete-entity-views-title": "{ count, plural, 1 {1 entityView} other {# entityViews}} varlık görünümüne sahip olmak istediğinizden emin misiniz?",
"delete-entity-views-action-title": "Sil { count, plural, 1 {1 entityView} other {# entityViews}}",
"delete-entity-views-title": "{ count, plural, 1 {1 entityView} other {# entityViews} } varlık görünümüne sahip olmak istediğinizden emin misiniz?",
"delete-entity-views-action-title": "Sil { count, plural, 1 {1 entityView} other {# entityViews} }",
"delete-entity-views-text": "Dikkatli olun, onaylandıktan sonra tüm seçilen görünümler kaldırılacak ve ilgili tüm veriler kurtarılamayacaktır.",
"unassign-entity-view-title": "Varlık görünümünün atamasını kaldırmak istediğinizden emin misiniz? {{entityViewName}} '? ",
"unassign-entity-view-text": "Onaydan sonra varlık görünümü atanmamış olacak ve müşteri tarafından erişilemeyecektir.",
"unassign-entity-view": "Varlık görünümünün atamasını kaldır",
"unassign-entity-views-title": "{ count, plural, 1 {1 entityView} other {# entityViews}} hesabının atamasını kaldırmak istediğinizden emin misiniz?",
"unassign-entity-views-title": "{ count, plural, 1 {1 entityView} other {# entityViews} } hesabının atamasını kaldırmak istediğinizden emin misiniz?",
"unassign-entity-views-text": "Onaylandıktan sonra, seçilen tüm öğe görünümleri atamadan kaldırılacak ve müşteri tarafından erişilemeyecektir.",
"entity-view-type": "Varlık Görünümü türü",
"entity-view-type-required": "Varlık Görünümü türü gerekli.",
@ -861,7 +861,7 @@
},
"extension": {
"extensions": "Uzantılar",
"selected-extensions": "{ count, plural, 1 {1 uzantı} other {# extensions}} seçildi",
"selected-extensions": "{ count, plural, 1 {1 uzantı} other {# extensions} } seçildi",
"type": "Tür",
"key": "Anahtar",
"value": "Değer",
@ -875,7 +875,7 @@
"edit": "Uzantıyı düzenle",
"delete-extension-title": "{{ExtensionId}} uzantısını silmek istediğinizden emin misiniz? ",
"delete-extension-text": "Dikkatli olun, onaylamadan sonra uzantı ve ilgili tüm veriler kurtarılamaz.",
"delete-extensions-title": "{ count, plural, 1 {1 uzantı} other {# extensions}} silmek istediğinizden emin misiniz?",
"delete-extensions-title": "{ count, plural, 1 {1 uzantı} other {# extensions} } silmek istediğinizden emin misiniz?",
"delete-extensions-text": "Dikkatli olun, onaylandıktan sonra tüm seçilen uzantılar kaldırılacak.",
"converters": "Dönüştürücü",
"converter-id": "Dönüştürücü kimliği",
@ -1606,4 +1606,4 @@
"cs_CZ": "Çekçe"
}
}
}
}

6
ui-ngx/src/assets/locale/locale.constant-uk_UA.json

@ -457,8 +457,8 @@
"customer-details": "Інформація про клієнта",
"delete-customer-title": "Ви впевнені, що хочете видалити клієнта '{{customerTitle}}'?",
"delete-customer-text": "Будьте обережні, після підтвердження, клієнт та всі пов'язані з ним дані, стануть недоступними.",
"delete-customers-title": "Ви впевнені, що хочете видалити {count, plural, 1 {1 клієнт}, інші {# клієнти}}?",
"delete-customers-action-title": "Видалити{ count, plural, 1 {1 клієнт} other {# клієнти} }",
"delete-customers-title": "Ви впевнені, що хочете видалити { count, plural, 1 {1 клієнт} other {# клієнти} }?",
"delete-customers-action-title": "Видалити { count, plural, 1 {1 клієнт} other {# клієнти} }",
"delete-customers-text": "Будьте обережні, після підтвердження, всі вибрані клієнти будуть видалені і всі пов'язані з ними дані, стануть недоступними.",
"manage-users": "Керування користувачами",
"manage-assets": "Керування активами",
@ -474,7 +474,7 @@
"select-customer": "Виберіть клієнта",
"no-customers-matching": "Клієнтів, які відповідають '{{entity}}' не знайдено.",
"customer-required": "Необхідно задати клієнта",
"selected-customers": "{ count, plural, 1 {1 клієнт} інші {# клієнти} } вибрано",
"selected-customers": "{ count, plural, 1 {1 клієнт} other {# клієнти} } вибрано",
"search": "Пошук клієнтів",
"select-group-to-add": "Виберіть цільову групу, щоб додати вибраних клієнтів",
"select-group-to-move": "Виберіть цільову групу для переміщення вибраних клієнтів",

16
ui-ngx/src/assets/locale/locale.constant-zh_CN.json

@ -596,11 +596,11 @@
"manage-credentials": "管理凭据",
"delete": "删除设备",
"assign-devices": "分配设备",
"assign-devices-text": "将{count,plural,1 {1 设备} other {# 设备}}分配给客户",
"assign-devices-text": "将{count,plural,1 {1 设备} other {# 设备} }分配给客户",
"delete-devices": "删除设备",
"unassign-from-customer": "取消分配客户",
"unassign-devices": "取消分配设备",
"unassign-devices-action-title": "从客户处取消分配{count,plural,1 {1 设备} other {# 设备}}",
"unassign-devices-action-title": "从客户处取消分配{count,plural,1 {1 设备} other {# 设备} }",
"assign-new-device": "分配新设备",
"make-public-device-title": "您确定要将设备 '{{deviceName}}' 设为公开吗?",
"make-public-device-text": "确认后,设备及其所有数据将被设为公开并可被其他人访问。",
@ -609,13 +609,13 @@
"view-credentials": "查看凭据",
"delete-device-title": "您确定要删除设备的{{deviceName}}吗?",
"delete-device-text": "小心!确认后设备及其所有相关数据将不可恢复。",
"delete-devices-title": "您确定要删除{count,plural,1 {1 设备} other {# 设备}} 吗?",
"delete-devices-action-title": "删除 {count,plural,1 {1 设备} other {# 设备}}",
"delete-devices-title": "您确定要删除{count,plural,1 {1 设备} other {# 设备} } 吗?",
"delete-devices-action-title": "删除 {count,plural,1 {1 设备} other {# 设备} }",
"delete-devices-text": "小心!确认后所有选定的设备将被删除,所有相关数据将不可恢复。",
"unassign-device-title": "您确定要取消分配设备 '{{deviceName}}'?",
"unassign-device-text": "确认后,设备将被取消分配,客户将无法访问。",
"unassign-device": "取消分配设备",
"unassign-devices-title": "您确定要取消分配{count,plural,1 {1 设备} other {# 设备}} 吗?",
"unassign-devices-title": "您确定要取消分配{count,plural,1 {1 设备} other {# 设备} } 吗?",
"unassign-devices-text": "确认后,所有选定的设备将被取消分配,并且客户将无法访问。",
"device-credentials": "设备凭据",
"credentials-type": "凭据类型",
@ -792,7 +792,7 @@
"delete-entity-views": "删除实体视图",
"unassign-from-customer": "取消分配客户",
"unassign-entity-views": "取消分配实体视图",
"unassign-entity-views-action-title": "从客户处取消分配{count,plural,1 {1 实体视图} other {# 实体视图}}",
"unassign-entity-views-action-title": "从客户处取消分配{count,plural,1 {1 实体视图} other {# 实体视图} }",
"assign-new-entity-view": "分配新实体视图",
"delete-entity-view-title": "确定要删除实体视图 '{{entityViewName}}'?",
"delete-entity-view-text": "小心!确认后实体视图及其所有相关数据将不可恢复。",
@ -1192,7 +1192,7 @@
"set-root-rulechain-text": "确认之后,规则链将变为根规格链,并将处理所有传入的传输消息。",
"delete-rulechain-title": " 确实要删除规则链'{{ruleChainName}}'吗?",
"delete-rulechain-text": "小心,在确认规则链和所有相关数据将变得不可恢复。",
"delete-rulechains-title": "确实要删除{count, plural, 1 { 1 规则链}其他{# 规则链库}}吗?",
"delete-rulechains-title": "确实要删除{count, plural, 1 { 1 规则链} other {# 规则链库} }吗?",
"delete-rulechains-action-title": "删除 { count, plural, 1 {1 规则链} other {# 规则链库} }",
"delete-rulechains-text": "小心,确认后,所有选定的规则链将被删除,所有相关的数据将变得不可恢复。",
"add-rulechain-text": "添加新的规则链",
@ -1283,7 +1283,7 @@
"tenant-details": "租客详情",
"delete-tenant-title": "您确定要删除租户'{{tenantTitle}}'吗?",
"delete-tenant-text": "小心!确认后,租户和所有相关数据将不可恢复。",
"delete-tenants-title": "您确定要删除 {count,plural,1 {1 租户} other {# 租户}} 吗?",
"delete-tenants-title": "您确定要删除 {count,plural,1 {1 租户} other {# 租户} } 吗?",
"delete-tenants-action-title": "删除 { count, plural, 1 {1 租户} other {# 租户} }",
"delete-tenants-text": "小心!确认后,所有选定的租户将被删除,所有相关数据将不可恢复。",
"title": "标题",

Loading…
Cancel
Save