Showing
93 changed files
with
6717 additions
and
145 deletions
ui-ngx/src/app/core/http/admin.service.ts
0 → 100644
1 | +/// | ||
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | ||
3 | +/// | ||
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | +/// you may not use this file except in compliance with the License. | ||
6 | +/// You may obtain a copy of the License at | ||
7 | +/// | ||
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | +/// | ||
10 | +/// Unless required by applicable law or agreed to in writing, software | ||
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | +/// See the License for the specific language governing permissions and | ||
14 | +/// limitations under the License. | ||
15 | +/// | ||
16 | + | ||
17 | +import { Injectable } from '@angular/core'; | ||
18 | +import { defaultHttpOptions } from './http-utils'; | ||
19 | +import { Observable } from 'rxjs/index'; | ||
20 | +import { HttpClient } from '@angular/common/http'; | ||
21 | +import {AdminSettings, MailServerSettings, SecuritySettings} from '@shared/models/settings.models'; | ||
22 | + | ||
23 | +@Injectable({ | ||
24 | + providedIn: 'root' | ||
25 | +}) | ||
26 | +export class AdminService { | ||
27 | + | ||
28 | + constructor( | ||
29 | + private http: HttpClient | ||
30 | + ) { } | ||
31 | + | ||
32 | + public getAdminSettings<T>(key: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<AdminSettings<T>> { | ||
33 | + return this.http.get<AdminSettings<T>>(`/api/admin/settings/${key}`, defaultHttpOptions(ignoreLoading, ignoreErrors)); | ||
34 | + } | ||
35 | + | ||
36 | + public saveAdminSettings<T>(adminSettings: AdminSettings<T>, | ||
37 | + ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<AdminSettings<T>> { | ||
38 | + return this.http.post<AdminSettings<T>>('/api/admin/settings', adminSettings, defaultHttpOptions(ignoreLoading, ignoreErrors)); | ||
39 | + } | ||
40 | + | ||
41 | + public sendTestMail(adminSettings: AdminSettings<MailServerSettings>, | ||
42 | + ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<void> { | ||
43 | + return this.http.post<void>('/api/admin/settings/testMail', adminSettings, defaultHttpOptions(ignoreLoading, ignoreErrors)); | ||
44 | + } | ||
45 | + | ||
46 | + public getSecuritySettings(ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<SecuritySettings> { | ||
47 | + return this.http.get<SecuritySettings>(`/api/admin/securitySettings`, defaultHttpOptions(ignoreLoading, ignoreErrors)); | ||
48 | + } | ||
49 | + | ||
50 | + public saveSecuritySettings(securitySettings: SecuritySettings, | ||
51 | + ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<SecuritySettings> { | ||
52 | + return this.http.post<SecuritySettings>('/api/admin/securitySettings', securitySettings, | ||
53 | + defaultHttpOptions(ignoreLoading, ignoreErrors)); | ||
54 | + } | ||
55 | +} |
ui-ngx/src/app/core/http/customer.service.ts
0 → 100644
1 | +/// | ||
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | ||
3 | +/// | ||
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | +/// you may not use this file except in compliance with the License. | ||
6 | +/// You may obtain a copy of the License at | ||
7 | +/// | ||
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | +/// | ||
10 | +/// Unless required by applicable law or agreed to in writing, software | ||
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | +/// See the License for the specific language governing permissions and | ||
14 | +/// limitations under the License. | ||
15 | +/// | ||
16 | + | ||
17 | +import { Injectable } from '@angular/core'; | ||
18 | +import { defaultHttpOptions } from './http-utils'; | ||
19 | +import { Observable } from 'rxjs/index'; | ||
20 | +import { HttpClient } from '@angular/common/http'; | ||
21 | +import { PageLink } from '@shared/models/page/page-link'; | ||
22 | +import { PageData } from '@shared/models/page/page-data'; | ||
23 | +import { Customer } from '@shared/models/customer.model'; | ||
24 | + | ||
25 | +@Injectable({ | ||
26 | + providedIn: 'root' | ||
27 | +}) | ||
28 | +export class CustomerService { | ||
29 | + | ||
30 | + constructor( | ||
31 | + private http: HttpClient | ||
32 | + ) { } | ||
33 | + | ||
34 | + public getCustomers(tenantId: string, pageLink: PageLink, ignoreErrors: boolean = false, | ||
35 | + ignoreLoading: boolean = false): Observable<PageData<Customer>> { | ||
36 | + return this.http.get<PageData<Customer>>(`/api/tenant/${tenantId}/customers${pageLink.toQuery()}`, | ||
37 | + defaultHttpOptions(ignoreLoading, ignoreErrors)); | ||
38 | + } | ||
39 | + | ||
40 | + public getCustomer(customerId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Customer> { | ||
41 | + return this.http.get<Customer>(`/api/customer/${customerId}`, defaultHttpOptions(ignoreLoading, ignoreErrors)); | ||
42 | + } | ||
43 | + | ||
44 | + public saveCustomer(customer: Customer, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Customer> { | ||
45 | + return this.http.post<Customer>('/api/customer', customer, defaultHttpOptions(ignoreLoading, ignoreErrors)); | ||
46 | + } | ||
47 | + | ||
48 | + public deleteCustomer(customerId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false) { | ||
49 | + return this.http.delete(`/api/customer/${customerId}`, defaultHttpOptions(ignoreLoading, ignoreErrors)); | ||
50 | + } | ||
51 | + | ||
52 | +} |
1 | +/// | ||
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | ||
3 | +/// | ||
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | +/// you may not use this file except in compliance with the License. | ||
6 | +/// You may obtain a copy of the License at | ||
7 | +/// | ||
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | +/// | ||
10 | +/// Unless required by applicable law or agreed to in writing, software | ||
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | +/// See the License for the specific language governing permissions and | ||
14 | +/// limitations under the License. | ||
15 | +/// | ||
16 | + | ||
17 | +import { Injectable } from '@angular/core'; | ||
18 | +import { defaultHttpOptions } from './http-utils'; | ||
19 | +import { Observable } from 'rxjs/index'; | ||
20 | +import { HttpClient } from '@angular/common/http'; | ||
21 | +import { PageLink } from '@shared/models/page/page-link'; | ||
22 | +import { PageData } from '@shared/models/page/page-data'; | ||
23 | +import { Tenant } from '@shared/models/tenant.model'; | ||
24 | +import {DashboardInfo, Dashboard} from '@shared/models/dashboard.models'; | ||
25 | +import {map} from 'rxjs/operators'; | ||
26 | + | ||
27 | +@Injectable({ | ||
28 | + providedIn: 'root' | ||
29 | +}) | ||
30 | +export class DashboardService { | ||
31 | + | ||
32 | + constructor( | ||
33 | + private http: HttpClient | ||
34 | + ) { } | ||
35 | + | ||
36 | + public getTenantDashboards(pageLink: PageLink, ignoreErrors: boolean = false, | ||
37 | + ignoreLoading: boolean = false): Observable<PageData<DashboardInfo>> { | ||
38 | + return this.http.get<PageData<DashboardInfo>>(`/api/tenant/dashboards${pageLink.toQuery()}`, | ||
39 | + defaultHttpOptions(ignoreLoading, ignoreErrors)); | ||
40 | + } | ||
41 | + | ||
42 | + public getTenantDashboardsByTenantId(tenantId: string, pageLink: PageLink, ignoreErrors: boolean = false, | ||
43 | + ignoreLoading: boolean = false): Observable<PageData<DashboardInfo>> { | ||
44 | + return this.http.get<PageData<DashboardInfo>>(`/api/tenant/${tenantId}/dashboards${pageLink.toQuery()}`, | ||
45 | + defaultHttpOptions(ignoreLoading, ignoreErrors)); | ||
46 | + } | ||
47 | + | ||
48 | + public getCustomerDashboards(customerId: string, pageLink: PageLink, ignoreErrors: boolean = false, | ||
49 | + ignoreLoading: boolean = false): Observable<PageData<DashboardInfo>> { | ||
50 | + return this.http.get<PageData<DashboardInfo>>(`/api/customer/${customerId}/dashboards${pageLink.toQuery()}`, | ||
51 | + defaultHttpOptions(ignoreLoading, ignoreErrors)).pipe( | ||
52 | + map( dashboards => { | ||
53 | + dashboards.data = dashboards.data.filter(dashboard => { | ||
54 | + return dashboard.title.toUpperCase().includes(pageLink.textSearch.toUpperCase()); | ||
55 | + }); | ||
56 | + return dashboards; | ||
57 | + } | ||
58 | + )); | ||
59 | + } | ||
60 | + | ||
61 | + public getDashboard(dashboardId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Dashboard> { | ||
62 | + return this.http.get<Dashboard>(`/api/dashboard/${dashboardId}`, defaultHttpOptions(ignoreLoading, ignoreErrors)); | ||
63 | + } | ||
64 | + | ||
65 | + public getDashboardInfo(dashboardId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<DashboardInfo> { | ||
66 | + return this.http.get<DashboardInfo>(`/api/dashboard/info/${dashboardId}`, defaultHttpOptions(ignoreLoading, ignoreErrors)); | ||
67 | + } | ||
68 | + | ||
69 | + public saveDashboard(dashboard: Dashboard, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Dashboard> { | ||
70 | + return this.http.post<Dashboard>('/api/dashboard', dashboard, defaultHttpOptions(ignoreLoading, ignoreErrors)); | ||
71 | + } | ||
72 | + | ||
73 | + public deleteDashboard(dashboardId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false) { | ||
74 | + return this.http.delete(`/api/dashboard/${dashboardId}`, defaultHttpOptions(ignoreLoading, ignoreErrors)); | ||
75 | + } | ||
76 | + | ||
77 | +} |
ui-ngx/src/app/core/http/tenant.service.ts
0 → 100644
1 | +/// | ||
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | ||
3 | +/// | ||
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | +/// you may not use this file except in compliance with the License. | ||
6 | +/// You may obtain a copy of the License at | ||
7 | +/// | ||
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | +/// | ||
10 | +/// Unless required by applicable law or agreed to in writing, software | ||
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | +/// See the License for the specific language governing permissions and | ||
14 | +/// limitations under the License. | ||
15 | +/// | ||
16 | + | ||
17 | +import { Injectable } from '@angular/core'; | ||
18 | +import { defaultHttpOptions } from './http-utils'; | ||
19 | +import { Observable } from 'rxjs/index'; | ||
20 | +import { HttpClient } from '@angular/common/http'; | ||
21 | +import { PageLink } from '@shared/models/page/page-link'; | ||
22 | +import { PageData } from '@shared/models/page/page-data'; | ||
23 | +import { Tenant } from '@shared/models/tenant.model'; | ||
24 | + | ||
25 | +@Injectable({ | ||
26 | + providedIn: 'root' | ||
27 | +}) | ||
28 | +export class TenantService { | ||
29 | + | ||
30 | + constructor( | ||
31 | + private http: HttpClient | ||
32 | + ) { } | ||
33 | + | ||
34 | + public getTenants(pageLink: PageLink, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<PageData<Tenant>> { | ||
35 | + return this.http.get<PageData<Tenant>>(`/api/tenants${pageLink.toQuery()}`, defaultHttpOptions(ignoreLoading, ignoreErrors)); | ||
36 | + } | ||
37 | + | ||
38 | + public getTenant(tenantId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Tenant> { | ||
39 | + return this.http.get<Tenant>(`/api/tenant/${tenantId}`, defaultHttpOptions(ignoreLoading, ignoreErrors)); | ||
40 | + } | ||
41 | + | ||
42 | + public saveTenant(tenant: Tenant, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Tenant> { | ||
43 | + return this.http.post<Tenant>('/api/tenant', tenant, defaultHttpOptions(ignoreLoading, ignoreErrors)); | ||
44 | + } | ||
45 | + | ||
46 | + public deleteTenant(tenantId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false) { | ||
47 | + return this.http.delete(`/api/tenant/${tenantId}`, defaultHttpOptions(ignoreLoading, ignoreErrors)); | ||
48 | + } | ||
49 | + | ||
50 | +} |
@@ -320,15 +320,78 @@ export class MenuService { | @@ -320,15 +320,78 @@ export class MenuService { | ||
320 | type: 'link', | 320 | type: 'link', |
321 | path: '/home', | 321 | path: '/home', |
322 | icon: 'home' | 322 | icon: 'home' |
323 | + }, | ||
324 | + { | ||
325 | + name: 'asset.assets', | ||
326 | + type: 'link', | ||
327 | + path: '/assets', | ||
328 | + icon: 'domain' | ||
329 | + }, | ||
330 | + { | ||
331 | + name: 'device.devices', | ||
332 | + type: 'link', | ||
333 | + path: '/devices', | ||
334 | + icon: 'devices_other' | ||
335 | + }, | ||
336 | + { | ||
337 | + name: 'entity-view.entity-views', | ||
338 | + type: 'link', | ||
339 | + path: '/entityViews', | ||
340 | + icon: 'view_quilt' | ||
341 | + }, | ||
342 | + { | ||
343 | + name: 'dashboard.dashboards', | ||
344 | + type: 'link', | ||
345 | + path: '/dashboards', | ||
346 | + icon: 'dashboard' | ||
323 | } | 347 | } |
324 | ); | 348 | ); |
325 | - // TODO: | ||
326 | return sections; | 349 | return sections; |
327 | } | 350 | } |
328 | 351 | ||
329 | private buildCustomerUserHome(authUser: any): Array<HomeSection> { | 352 | private buildCustomerUserHome(authUser: any): Array<HomeSection> { |
330 | - const homeSections: Array<HomeSection> = []; | ||
331 | - // TODO: | 353 | + const homeSections: Array<HomeSection> = [ |
354 | + { | ||
355 | + name: 'asset.view-assets', | ||
356 | + places: [ | ||
357 | + { | ||
358 | + name: 'asset.assets', | ||
359 | + icon: 'domain', | ||
360 | + path: '/assets' | ||
361 | + } | ||
362 | + ] | ||
363 | + }, | ||
364 | + { | ||
365 | + name: 'device.view-devices', | ||
366 | + places: [ | ||
367 | + { | ||
368 | + name: 'device.devices', | ||
369 | + icon: 'devices_other', | ||
370 | + path: '/devices' | ||
371 | + } | ||
372 | + ] | ||
373 | + }, | ||
374 | + { | ||
375 | + name: 'entity-view.management', | ||
376 | + places: [ | ||
377 | + { | ||
378 | + name: 'entity-view.entity-views', | ||
379 | + icon: 'view_quilt', | ||
380 | + path: '/entityViews' | ||
381 | + } | ||
382 | + ] | ||
383 | + }, | ||
384 | + { | ||
385 | + name: 'dashboard.view-dashboards', | ||
386 | + places: [ | ||
387 | + { | ||
388 | + name: 'dashboard.dashboards', | ||
389 | + icon: 'dashboard', | ||
390 | + path: '/dashboards' | ||
391 | + } | ||
392 | + ] | ||
393 | + } | ||
394 | + ]; | ||
332 | return homeSections; | 395 | return homeSections; |
333 | } | 396 | } |
334 | 397 |
@@ -56,8 +56,14 @@ export class TranslateDefaultCompiler extends TranslateMessageFormatCompiler { | @@ -56,8 +56,14 @@ export class TranslateDefaultCompiler extends TranslateMessageFormatCompiler { | ||
56 | } | 56 | } |
57 | 57 | ||
58 | private checkIsPlural(src: string): boolean { | 58 | private checkIsPlural(src: string): boolean { |
59 | - const tokens: any[] = parse(src.replace(/\{\{/g, '{').replace(/\}\}/g, '}'), | ||
60 | - {cardinal: [], ordinal: []}); | 59 | + let tokens: any[]; |
60 | + try { | ||
61 | + tokens = parse(src.replace(/\{\{/g, '{').replace(/\}\}/g, '}'), | ||
62 | + {cardinal: [], ordinal: []}); | ||
63 | + } catch (e) { | ||
64 | + console.warn(`Failed to parse source: ${src}`); | ||
65 | + console.error(e); | ||
66 | + } | ||
61 | const res = tokens.filter( | 67 | const res = tokens.filter( |
62 | (value) => typeof value !== 'string' && value.type === 'plural' | 68 | (value) => typeof value !== 'string' && value.type === 'plural' |
63 | ); | 69 | ); |
@@ -106,7 +106,7 @@ | @@ -106,7 +106,7 @@ | ||
106 | button { | 106 | button { |
107 | padding: 0 16px 0 32px; | 107 | padding: 0 16px 0 32px; |
108 | font-weight: 500; | 108 | font-weight: 500; |
109 | - text-transform: none; | 109 | + text-transform: none !important; |
110 | text-rendering: optimizeLegibility; | 110 | text-rendering: optimizeLegibility; |
111 | } | 111 | } |
112 | } | 112 | } |
1 | +/// | ||
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | ||
3 | +/// | ||
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | +/// you may not use this file except in compliance with the License. | ||
6 | +/// You may obtain a copy of the License at | ||
7 | +/// | ||
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | +/// | ||
10 | +/// Unless required by applicable law or agreed to in writing, software | ||
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | +/// See the License for the specific language governing permissions and | ||
14 | +/// limitations under the License. | ||
15 | +/// | ||
16 | + | ||
17 | +import { NgModule } from '@angular/core'; | ||
18 | +import { Routes, RouterModule } from '@angular/router'; | ||
19 | + | ||
20 | +import { MailServerComponent } from '@modules/home/pages/admin/mail-server.component'; | ||
21 | +import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard'; | ||
22 | +import { Authority } from '@shared/models/authority.enum'; | ||
23 | +import {GeneralSettingsComponent} from "@modules/home/pages/admin/general-settings.component"; | ||
24 | +import {SecuritySettingsComponent} from "@modules/home/pages/admin/security-settings.component"; | ||
25 | + | ||
26 | +const routes: Routes = [ | ||
27 | + { | ||
28 | + path: 'settings', | ||
29 | + data: { | ||
30 | + auth: [Authority.SYS_ADMIN], | ||
31 | + breadcrumb: { | ||
32 | + label: 'admin.system-settings', | ||
33 | + icon: 'settings' | ||
34 | + } | ||
35 | + }, | ||
36 | + children: [ | ||
37 | + { | ||
38 | + path: '', | ||
39 | + redirectTo: 'general', | ||
40 | + pathMatch: 'full' | ||
41 | + }, | ||
42 | + { | ||
43 | + path: 'general', | ||
44 | + component: GeneralSettingsComponent, | ||
45 | + canDeactivate: [ConfirmOnExitGuard], | ||
46 | + data: { | ||
47 | + auth: [Authority.SYS_ADMIN], | ||
48 | + title: 'admin.general-settings', | ||
49 | + breadcrumb: { | ||
50 | + label: 'admin.general', | ||
51 | + icon: 'settings_applications' | ||
52 | + } | ||
53 | + } | ||
54 | + }, | ||
55 | + { | ||
56 | + path: 'outgoing-mail', | ||
57 | + component: MailServerComponent, | ||
58 | + canDeactivate: [ConfirmOnExitGuard], | ||
59 | + data: { | ||
60 | + auth: [Authority.SYS_ADMIN], | ||
61 | + title: 'admin.outgoing-mail-settings', | ||
62 | + breadcrumb: { | ||
63 | + label: 'admin.outgoing-mail', | ||
64 | + icon: 'mail' | ||
65 | + } | ||
66 | + } | ||
67 | + }, | ||
68 | + { | ||
69 | + path: 'security-settings', | ||
70 | + component: SecuritySettingsComponent, | ||
71 | + canDeactivate: [ConfirmOnExitGuard], | ||
72 | + data: { | ||
73 | + auth: [Authority.SYS_ADMIN], | ||
74 | + title: 'admin.security-settings', | ||
75 | + breadcrumb: { | ||
76 | + label: 'admin.security-settings', | ||
77 | + icon: 'security' | ||
78 | + } | ||
79 | + } | ||
80 | + } | ||
81 | + ] | ||
82 | + } | ||
83 | +]; | ||
84 | + | ||
85 | +@NgModule({ | ||
86 | + imports: [RouterModule.forChild(routes)], | ||
87 | + exports: [RouterModule] | ||
88 | +}) | ||
89 | +export class AdminRoutingModule { } |
1 | +/// | ||
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | ||
3 | +/// | ||
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | +/// you may not use this file except in compliance with the License. | ||
6 | +/// You may obtain a copy of the License at | ||
7 | +/// | ||
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | +/// | ||
10 | +/// Unless required by applicable law or agreed to in writing, software | ||
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | +/// See the License for the specific language governing permissions and | ||
14 | +/// limitations under the License. | ||
15 | +/// | ||
16 | + | ||
17 | +import { NgModule } from '@angular/core'; | ||
18 | +import { CommonModule } from '@angular/common'; | ||
19 | + | ||
20 | +import { AdminRoutingModule } from './admin-routing.module'; | ||
21 | +import { SharedModule } from '@app/shared/shared.module'; | ||
22 | +import { MailServerComponent } from '@modules/home/pages/admin/mail-server.component'; | ||
23 | +import {GeneralSettingsComponent} from "@modules/home/pages/admin/general-settings.component"; | ||
24 | +import {SecuritySettingsComponent} from "@modules/home/pages/admin/security-settings.component"; | ||
25 | + | ||
26 | +@NgModule({ | ||
27 | + declarations: | ||
28 | + [ | ||
29 | + GeneralSettingsComponent, | ||
30 | + MailServerComponent, | ||
31 | + SecuritySettingsComponent | ||
32 | + ], | ||
33 | + imports: [ | ||
34 | + CommonModule, | ||
35 | + SharedModule, | ||
36 | + AdminRoutingModule | ||
37 | + ] | ||
38 | +}) | ||
39 | +export class AdminModule { } |
1 | +<!-- | ||
2 | + | ||
3 | + Copyright © 2016-2019 The Thingsboard Authors | ||
4 | + | ||
5 | + Licensed under the Apache License, Version 2.0 (the "License"); | ||
6 | + you may not use this file except in compliance with the License. | ||
7 | + You may obtain a copy of the License at | ||
8 | + | ||
9 | + http://www.apache.org/licenses/LICENSE-2.0 | ||
10 | + | ||
11 | + Unless required by applicable law or agreed to in writing, software | ||
12 | + distributed under the License is distributed on an "AS IS" BASIS, | ||
13 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
14 | + See the License for the specific language governing permissions and | ||
15 | + limitations under the License. | ||
16 | + | ||
17 | +--> | ||
18 | +<div> | ||
19 | + <mat-card class="settings-card"> | ||
20 | + <mat-card-title> | ||
21 | + <div fxLayout="row"> | ||
22 | + <span class="mat-headline" translate>admin.general-settings</span> | ||
23 | + </div> | ||
24 | + </mat-card-title> | ||
25 | + <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> | ||
26 | + </mat-progress-bar> | ||
27 | + <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div> | ||
28 | + <mat-card-content style="padding-top: 16px;"> | ||
29 | + <form #generalSettingsForm="ngForm" [formGroup]="generalSettings" (ngSubmit)="save()"> | ||
30 | + <fieldset [disabled]="isLoading$ | async"> | ||
31 | + <mat-form-field class="mat-block"> | ||
32 | + <mat-label translate>admin.base-url</mat-label> | ||
33 | + <input matInput formControlName="baseUrl" required/> | ||
34 | + <mat-error *ngIf="generalSettings.get('baseUrl').hasError('required')"> | ||
35 | + {{ 'admin.base-url-required' | translate }} | ||
36 | + </mat-error> | ||
37 | + </mat-form-field> | ||
38 | + <div fxLayout="row" fxLayoutAlign="end center" style="width: 100%;" class="layout-wrap"> | ||
39 | + <button mat-button mat-raised-button color="primary" [disabled]="(isLoading$ | async) || generalSettingsForm.invalid || !generalSettingsForm.dirty" | ||
40 | + type="submit">{{'action.save' | translate}} | ||
41 | + </button> | ||
42 | + </div> | ||
43 | + </fieldset> | ||
44 | + </form> | ||
45 | + </mat-card-content> | ||
46 | + </mat-card> | ||
47 | +</div> |
1 | +/** | ||
2 | + * Copyright © 2016-2019 The Thingsboard Authors | ||
3 | + * | ||
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | + * you may not use this file except in compliance with the License. | ||
6 | + * You may obtain a copy of the License at | ||
7 | + * | ||
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | + * | ||
10 | + * Unless required by applicable law or agreed to in writing, software | ||
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | + * See the License for the specific language governing permissions and | ||
14 | + * limitations under the License. | ||
15 | + */ | ||
16 | +:host { | ||
17 | + | ||
18 | +} |
1 | +/// | ||
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | ||
3 | +/// | ||
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | +/// you may not use this file except in compliance with the License. | ||
6 | +/// You may obtain a copy of the License at | ||
7 | +/// | ||
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | +/// | ||
10 | +/// Unless required by applicable law or agreed to in writing, software | ||
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | +/// See the License for the specific language governing permissions and | ||
14 | +/// limitations under the License. | ||
15 | +/// | ||
16 | + | ||
17 | +import { Component, OnInit } from '@angular/core'; | ||
18 | +import { Store } from '@ngrx/store'; | ||
19 | +import { AppState } from '@core/core.state'; | ||
20 | +import { PageComponent } from '@shared/components/page.component'; | ||
21 | +import { Router } from '@angular/router'; | ||
22 | +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; | ||
23 | +import {AdminSettings, GeneralSettings} from '@shared/models/settings.models'; | ||
24 | +import { AdminService } from '@core/http/admin.service'; | ||
25 | +import { ActionNotificationShow } from '@core/notification/notification.actions'; | ||
26 | +import { TranslateService } from '@ngx-translate/core'; | ||
27 | +import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard'; | ||
28 | + | ||
29 | +@Component({ | ||
30 | + selector: 'tb-general-settings', | ||
31 | + templateUrl: './general-settings.component.html', | ||
32 | + styleUrls: ['./general-settings.component.scss', './settings-card.scss'] | ||
33 | +}) | ||
34 | +export class GeneralSettingsComponent extends PageComponent implements OnInit, HasConfirmForm { | ||
35 | + | ||
36 | + generalSettings: FormGroup; | ||
37 | + adminSettings: AdminSettings<GeneralSettings>; | ||
38 | + | ||
39 | + constructor(protected store: Store<AppState>, | ||
40 | + private router: Router, | ||
41 | + private adminService: AdminService, | ||
42 | + private translate: TranslateService, | ||
43 | + public fb: FormBuilder) { | ||
44 | + super(store); | ||
45 | + } | ||
46 | + | ||
47 | + ngOnInit() { | ||
48 | + this.buildGeneralServerSettingsForm(); | ||
49 | + this.adminService.getAdminSettings<GeneralSettings>('general').subscribe( | ||
50 | + (adminSettings) => { | ||
51 | + this.adminSettings = adminSettings; | ||
52 | + this.generalSettings.reset(this.adminSettings.jsonValue); | ||
53 | + } | ||
54 | + ); | ||
55 | + } | ||
56 | + | ||
57 | + buildGeneralServerSettingsForm() { | ||
58 | + this.generalSettings = this.fb.group({ | ||
59 | + baseUrl: ['', [Validators.required]] | ||
60 | + }); | ||
61 | + } | ||
62 | + | ||
63 | + save(): void { | ||
64 | + this.adminSettings.jsonValue = {...this.adminSettings.jsonValue, ...this.generalSettings.value}; | ||
65 | + this.adminService.saveAdminSettings(this.adminSettings).subscribe( | ||
66 | + (adminSettings) => { | ||
67 | + this.adminSettings = adminSettings; | ||
68 | + this.generalSettings.reset(this.adminSettings.jsonValue); | ||
69 | + } | ||
70 | + ); | ||
71 | + } | ||
72 | + | ||
73 | + confirmForm(): FormGroup { | ||
74 | + return this.generalSettings; | ||
75 | + } | ||
76 | + | ||
77 | +} |
1 | +<!-- | ||
2 | + | ||
3 | + Copyright © 2016-2019 The Thingsboard Authors | ||
4 | + | ||
5 | + Licensed under the Apache License, Version 2.0 (the "License"); | ||
6 | + you may not use this file except in compliance with the License. | ||
7 | + You may obtain a copy of the License at | ||
8 | + | ||
9 | + http://www.apache.org/licenses/LICENSE-2.0 | ||
10 | + | ||
11 | + Unless required by applicable law or agreed to in writing, software | ||
12 | + distributed under the License is distributed on an "AS IS" BASIS, | ||
13 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
14 | + See the License for the specific language governing permissions and | ||
15 | + limitations under the License. | ||
16 | + | ||
17 | +--> | ||
18 | +<div> | ||
19 | + <mat-card class="settings-card"> | ||
20 | + <mat-card-title> | ||
21 | + <div fxLayout="row"> | ||
22 | + <span class="mat-headline" translate>admin.outgoing-mail-settings</span> | ||
23 | + <span fxFlex></span> | ||
24 | + <div tb-help="outgoingMailSettings"></div> | ||
25 | + </div> | ||
26 | + </mat-card-title> | ||
27 | + <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> | ||
28 | + </mat-progress-bar> | ||
29 | + <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div> | ||
30 | + <mat-card-content style="padding-top: 16px;"> | ||
31 | + <form #mailSettingsForm="ngForm" [formGroup]="mailSettings" (ngSubmit)="save()"> | ||
32 | + <fieldset [disabled]="isLoading$ | async"> | ||
33 | + <mat-form-field class="mat-block"> | ||
34 | + <mat-label translate>admin.mail-from</mat-label> | ||
35 | + <input matInput formControlName="mailFrom" required/> | ||
36 | + <mat-error *ngIf="mailSettings.get('mailFrom').hasError('required')"> | ||
37 | + {{ 'admin.mail-from-required' | translate }} | ||
38 | + </mat-error> | ||
39 | + </mat-form-field> | ||
40 | + <mat-form-field class="mat-block"> | ||
41 | + <mat-label translate>admin.smtp-protocol</mat-label> | ||
42 | + <mat-select matInput formControlName="smtpProtocol"> | ||
43 | + <mat-option *ngFor="let protocol of smtpProtocols" [value]="protocol"> | ||
44 | + {{protocol.toUpperCase()}} | ||
45 | + </mat-option> | ||
46 | + </mat-select> | ||
47 | + </mat-form-field> | ||
48 | + <div fxLayout.gt-sm="row" fxLayoutGap.gt-sm="10px"> | ||
49 | + <mat-form-field class="mat-block" fxFlex="100" fxFlex.gt-sm="60"> | ||
50 | + <mat-label translate>admin.smtp-host</mat-label> | ||
51 | + <input matInput formControlName="smtpHost" placeholder="localhost" required/> | ||
52 | + <mat-error *ngIf="mailSettings.get('smtpHost').hasError('required')"> | ||
53 | + {{ 'admin.smtp-host-required' | translate }} | ||
54 | + </mat-error> | ||
55 | + </mat-form-field> | ||
56 | + <mat-form-field class="mat-block" fxFlex="100" fxFlex.gt-sm="40"> | ||
57 | + <mat-label translate>admin.smtp-port</mat-label> | ||
58 | + <input matInput #smtpPortInput formControlName="smtpPort" placeholder="25" maxlength="5" required/> | ||
59 | + <mat-hint align="end">{{smtpPortInput.value?.length || 0}}/5</mat-hint> | ||
60 | + <mat-error *ngIf="mailSettings.get('smtpPort').hasError('required')"> | ||
61 | + {{ 'admin.smtp-port-required' | translate }} | ||
62 | + </mat-error> | ||
63 | + <mat-error *ngIf="mailSettings.get('smtpPort').hasError('pattern') || mailSettings.get('smtpPort').hasError('maxlength')"> | ||
64 | + {{ 'admin.smtp-port-invalid' | translate }} | ||
65 | + </mat-error> | ||
66 | + </mat-form-field> | ||
67 | + </div> | ||
68 | + <mat-form-field class="mat-block"> | ||
69 | + <mat-label translate>admin.timeout-msec</mat-label> | ||
70 | + <input matInput #timeoutInput formControlName="timeout" placeholder="10000" maxlength="6" required/> | ||
71 | + <mat-hint align="end">{{timeoutInput.value?.length || 0}}/6</mat-hint> | ||
72 | + <mat-error *ngIf="mailSettings.get('timeout').hasError('required')"> | ||
73 | + {{ 'admin.timeout-required' | translate }} | ||
74 | + </mat-error> | ||
75 | + <mat-error *ngIf="mailSettings.get('timeout').hasError('pattern') || mailSettings.get('timeout').hasError('maxlength')"> | ||
76 | + {{ 'admin.timeout-invalid' | translate }} | ||
77 | + </mat-error> | ||
78 | + </mat-form-field> | ||
79 | + <tb-checkbox formControlName="enableTls" trueValue="true" falseValue="false"> | ||
80 | + {{ 'admin.enable-tls' | translate }} | ||
81 | + </tb-checkbox> | ||
82 | + <mat-form-field class="mat-block"> | ||
83 | + <mat-label translate>common.username</mat-label> | ||
84 | + <input matInput formControlName="username" placeholder="{{ 'common.enter-username' | translate }}"/> | ||
85 | + </mat-form-field> | ||
86 | + <mat-form-field class="mat-block"> | ||
87 | + <mat-label translate>common.password</mat-label> | ||
88 | + <input matInput formControlName="password" type="password" placeholder="{{ 'common.enter-password' | translate }}"/> | ||
89 | + </mat-form-field> | ||
90 | + <div fxLayout="row" fxLayoutAlign="end center" style="width: 100%;" class="layout-wrap"> | ||
91 | + <button mat-button mat-raised-button | ||
92 | + type="button" style="margin-right: 16px;" | ||
93 | + [disabled]="(isLoading$ | async) || mailSettingsForm.invalid" (click)="sendTestMail()"> | ||
94 | + {{'admin.send-test-mail' | translate}} | ||
95 | + </button> | ||
96 | + <button mat-button mat-raised-button color="primary" [disabled]="(isLoading$ | async) || mailSettingsForm.invalid || !mailSettingsForm.dirty" | ||
97 | + type="submit">{{'action.save' | translate}} | ||
98 | + </button> | ||
99 | + </div> | ||
100 | + </fieldset> | ||
101 | + </form> | ||
102 | + </mat-card-content> | ||
103 | + </mat-card> | ||
104 | +</div> |
1 | +/** | ||
2 | + * Copyright © 2016-2019 The Thingsboard Authors | ||
3 | + * | ||
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | + * you may not use this file except in compliance with the License. | ||
6 | + * You may obtain a copy of the License at | ||
7 | + * | ||
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | + * | ||
10 | + * Unless required by applicable law or agreed to in writing, software | ||
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | + * See the License for the specific language governing permissions and | ||
14 | + * limitations under the License. | ||
15 | + */ | ||
16 | +:host { | ||
17 | + | ||
18 | +} |
1 | +/// | ||
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | ||
3 | +/// | ||
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | +/// you may not use this file except in compliance with the License. | ||
6 | +/// You may obtain a copy of the License at | ||
7 | +/// | ||
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | +/// | ||
10 | +/// Unless required by applicable law or agreed to in writing, software | ||
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | +/// See the License for the specific language governing permissions and | ||
14 | +/// limitations under the License. | ||
15 | +/// | ||
16 | + | ||
17 | +import { Component, OnInit } from '@angular/core'; | ||
18 | +import { Store } from '@ngrx/store'; | ||
19 | +import { AppState } from '@core/core.state'; | ||
20 | +import { PageComponent } from '@shared/components/page.component'; | ||
21 | +import { Router } from '@angular/router'; | ||
22 | +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; | ||
23 | +import { AdminSettings, MailServerSettings, smtpPortPattern } from '@shared/models/settings.models'; | ||
24 | +import { AdminService } from '@core/http/admin.service'; | ||
25 | +import { ActionNotificationShow } from '@core/notification/notification.actions'; | ||
26 | +import { TranslateService } from '@ngx-translate/core'; | ||
27 | +import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard'; | ||
28 | + | ||
29 | +@Component({ | ||
30 | + selector: 'tb-mail-server', | ||
31 | + templateUrl: './mail-server.component.html', | ||
32 | + styleUrls: ['./mail-server.component.scss', './settings-card.scss'] | ||
33 | +}) | ||
34 | +export class MailServerComponent extends PageComponent implements OnInit, HasConfirmForm { | ||
35 | + | ||
36 | + mailSettings: FormGroup; | ||
37 | + adminSettings: AdminSettings<MailServerSettings>; | ||
38 | + smtpProtocols = ['smtp', 'smtps']; | ||
39 | + | ||
40 | + constructor(protected store: Store<AppState>, | ||
41 | + private router: Router, | ||
42 | + private adminService: AdminService, | ||
43 | + private translate: TranslateService, | ||
44 | + public fb: FormBuilder) { | ||
45 | + super(store); | ||
46 | + } | ||
47 | + | ||
48 | + ngOnInit() { | ||
49 | + this.buildMailServerSettingsForm(); | ||
50 | + this.adminService.getAdminSettings<MailServerSettings>('mail').subscribe( | ||
51 | + (adminSettings) => { | ||
52 | + this.adminSettings = adminSettings; | ||
53 | + this.mailSettings.reset(this.adminSettings.jsonValue); | ||
54 | + } | ||
55 | + ); | ||
56 | + } | ||
57 | + | ||
58 | + buildMailServerSettingsForm() { | ||
59 | + this.mailSettings = this.fb.group({ | ||
60 | + mailFrom: ['', [Validators.required]], | ||
61 | + smtpProtocol: ['smtp'], | ||
62 | + smtpHost: ['localhost', [Validators.required]], | ||
63 | + smtpPort: ['25', [Validators.required, | ||
64 | + Validators.pattern(smtpPortPattern), | ||
65 | + Validators.maxLength(5)]], | ||
66 | + timeout: ['10000', [Validators.required, | ||
67 | + Validators.pattern(/^[0-9]{1,6}$/), | ||
68 | + Validators.maxLength(6)]], | ||
69 | + enableTls: ['false'], | ||
70 | + username: [''], | ||
71 | + password: [''] | ||
72 | + }); | ||
73 | + this.registerDisableOnLoadFormControl(this.mailSettings.get('smtpProtocol')); | ||
74 | + this.registerDisableOnLoadFormControl(this.mailSettings.get('enableTls')); | ||
75 | + } | ||
76 | + | ||
77 | + sendTestMail(): void { | ||
78 | + this.adminSettings.jsonValue = {...this.adminSettings.jsonValue, ...this.mailSettings.value}; | ||
79 | + this.adminService.sendTestMail(this.adminSettings).subscribe( | ||
80 | + () => { | ||
81 | + this.store.dispatch(new ActionNotificationShow({ message: this.translate.instant('admin.test-mail-sent'), | ||
82 | + type: 'success' })); | ||
83 | + } | ||
84 | + ); | ||
85 | + } | ||
86 | + | ||
87 | + save(): void { | ||
88 | + this.adminSettings.jsonValue = {...this.adminSettings.jsonValue, ...this.mailSettings.value}; | ||
89 | + this.adminService.saveAdminSettings(this.adminSettings).subscribe( | ||
90 | + (adminSettings) => { | ||
91 | + this.adminSettings = adminSettings; | ||
92 | + this.mailSettings.reset(this.adminSettings.jsonValue); | ||
93 | + } | ||
94 | + ); | ||
95 | + } | ||
96 | + | ||
97 | + confirmForm(): FormGroup { | ||
98 | + return this.mailSettings; | ||
99 | + } | ||
100 | + | ||
101 | +} |
1 | +<!-- | ||
2 | + | ||
3 | + Copyright © 2016-2019 The Thingsboard Authors | ||
4 | + | ||
5 | + Licensed under the Apache License, Version 2.0 (the "License"); | ||
6 | + you may not use this file except in compliance with the License. | ||
7 | + You may obtain a copy of the License at | ||
8 | + | ||
9 | + http://www.apache.org/licenses/LICENSE-2.0 | ||
10 | + | ||
11 | + Unless required by applicable law or agreed to in writing, software | ||
12 | + distributed under the License is distributed on an "AS IS" BASIS, | ||
13 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
14 | + See the License for the specific language governing permissions and | ||
15 | + limitations under the License. | ||
16 | + | ||
17 | +--> | ||
18 | +<div> | ||
19 | + <mat-card class="settings-card"> | ||
20 | + <mat-card-title> | ||
21 | + <div fxLayout="row"> | ||
22 | + <span class="mat-headline" translate>admin.security-settings</span> | ||
23 | + <span fxFlex></span> | ||
24 | + <div tb-help="securitySettings"></div> | ||
25 | + </div> | ||
26 | + </mat-card-title> | ||
27 | + <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> | ||
28 | + </mat-progress-bar> | ||
29 | + <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div> | ||
30 | + <mat-card-content style="padding-top: 16px;"> | ||
31 | + <form #securitySettingsForm="ngForm" [formGroup]="securitySettingsFormGroup" (ngSubmit)="save()"> | ||
32 | + <fieldset [disabled]="isLoading$ | async"> | ||
33 | + <mat-expansion-panel [expanded]="true"> | ||
34 | + <mat-expansion-panel-header> | ||
35 | + <mat-panel-title> | ||
36 | + <div class="tb-panel-title" translate>admin.password-policy</div> | ||
37 | + </mat-panel-title> | ||
38 | + </mat-expansion-panel-header> | ||
39 | + <section formGroupName="passwordPolicy"> | ||
40 | + <mat-form-field class="mat-block"> | ||
41 | + <mat-label translate>admin.minimum-password-length</mat-label> | ||
42 | + <input matInput type="number" | ||
43 | + formControlName="minimumLength" | ||
44 | + step="1" | ||
45 | + min="5" | ||
46 | + max="50" | ||
47 | + required/> | ||
48 | + <mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy').get('minimumLength').hasError('required')"> | ||
49 | + {{ 'admin.minimum-password-length-required' | translate }} | ||
50 | + </mat-error> | ||
51 | + <mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy').get('minimumLength').hasError('min')"> | ||
52 | + {{ 'admin.minimum-password-length-range' | translate }} | ||
53 | + </mat-error> | ||
54 | + <mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy').get('minimumLength').hasError('max')"> | ||
55 | + {{ 'admin.minimum-password-length-range' | translate }} | ||
56 | + </mat-error> | ||
57 | + </mat-form-field> | ||
58 | + <mat-form-field class="mat-block"> | ||
59 | + <mat-label translate>admin.minimum-uppercase-letters</mat-label> | ||
60 | + <input matInput type="number" | ||
61 | + formControlName="minimumUppercaseLetters" | ||
62 | + step="1" | ||
63 | + min="0"/> | ||
64 | + <mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy').get('minimumUppercaseLetters').hasError('min')"> | ||
65 | + {{ 'admin.minimum-uppercase-letters-range' | translate }} | ||
66 | + </mat-error> | ||
67 | + </mat-form-field> | ||
68 | + <mat-form-field class="mat-block"> | ||
69 | + <mat-label translate>admin.minimum-lowercase-letters</mat-label> | ||
70 | + <input matInput type="number" | ||
71 | + formControlName="minimumLowercaseLetters" | ||
72 | + step="1" | ||
73 | + min="0"/> | ||
74 | + <mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy').get('minimumLowercaseLetters').hasError('min')"> | ||
75 | + {{ 'admin.minimum-lowercase-letters-range' | translate }} | ||
76 | + </mat-error> | ||
77 | + </mat-form-field> | ||
78 | + <mat-form-field class="mat-block"> | ||
79 | + <mat-label translate>admin.minimum-digits</mat-label> | ||
80 | + <input matInput type="number" | ||
81 | + formControlName="minimumDigits" | ||
82 | + step="1" | ||
83 | + min="0"/> | ||
84 | + <mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy').get('minimumDigits').hasError('min')"> | ||
85 | + {{ 'admin.minimum-digits-range' | translate }} | ||
86 | + </mat-error> | ||
87 | + </mat-form-field> | ||
88 | + <mat-form-field class="mat-block"> | ||
89 | + <mat-label translate>admin.minimum-special-characters</mat-label> | ||
90 | + <input matInput type="number" | ||
91 | + formControlName="minimumSpecialCharacters" | ||
92 | + step="1" | ||
93 | + min="0"/> | ||
94 | + <mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy').get('minimumSpecialCharacters').hasError('min')"> | ||
95 | + {{ 'admin.minimum-special-characters-range' | translate }} | ||
96 | + </mat-error> | ||
97 | + </mat-form-field> | ||
98 | + <mat-form-field class="mat-block"> | ||
99 | + <mat-label translate>admin.password-expiration-period-days</mat-label> | ||
100 | + <input matInput type="number" | ||
101 | + formControlName="passwordExpirationPeriodDays" | ||
102 | + step="1" | ||
103 | + min="0"/> | ||
104 | + <mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy').get('passwordExpirationPeriodDays').hasError('min')"> | ||
105 | + {{ 'admin.password-expiration-period-days-range' | translate }} | ||
106 | + </mat-error> | ||
107 | + </mat-form-field> | ||
108 | + </section> | ||
109 | + </mat-expansion-panel> | ||
110 | + <div fxLayout="row" fxLayoutAlign="end center" style="width: 100%;" class="layout-wrap"> | ||
111 | + <button mat-button mat-raised-button color="primary" [disabled]="(isLoading$ | async) || securitySettingsForm.invalid || !securitySettingsForm.dirty" | ||
112 | + type="submit">{{'action.save' | translate}} | ||
113 | + </button> | ||
114 | + </div> | ||
115 | + </fieldset> | ||
116 | + </form> | ||
117 | + </mat-card-content> | ||
118 | + </mat-card> | ||
119 | +</div> |
1 | +/** | ||
2 | + * Copyright © 2016-2019 The Thingsboard Authors | ||
3 | + * | ||
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | + * you may not use this file except in compliance with the License. | ||
6 | + * You may obtain a copy of the License at | ||
7 | + * | ||
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | + * | ||
10 | + * Unless required by applicable law or agreed to in writing, software | ||
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | + * See the License for the specific language governing permissions and | ||
14 | + * limitations under the License. | ||
15 | + */ | ||
16 | +:host { | ||
17 | + mat-expansion-panel { | ||
18 | + margin-bottom: 16px; | ||
19 | + } | ||
20 | + .tb-panel-title { | ||
21 | + | ||
22 | + } | ||
23 | +} |
1 | +/// | ||
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | ||
3 | +/// | ||
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | +/// you may not use this file except in compliance with the License. | ||
6 | +/// You may obtain a copy of the License at | ||
7 | +/// | ||
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | +/// | ||
10 | +/// Unless required by applicable law or agreed to in writing, software | ||
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | +/// See the License for the specific language governing permissions and | ||
14 | +/// limitations under the License. | ||
15 | +/// | ||
16 | + | ||
17 | +import { Component, OnInit } from '@angular/core'; | ||
18 | +import { Store } from '@ngrx/store'; | ||
19 | +import { AppState } from '@core/core.state'; | ||
20 | +import { PageComponent } from '@shared/components/page.component'; | ||
21 | +import { Router } from '@angular/router'; | ||
22 | +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; | ||
23 | +import { SecuritySettings} from '@shared/models/settings.models'; | ||
24 | +import { AdminService } from '@core/http/admin.service'; | ||
25 | +import { ActionNotificationShow } from '@core/notification/notification.actions'; | ||
26 | +import { TranslateService } from '@ngx-translate/core'; | ||
27 | +import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard'; | ||
28 | + | ||
29 | +@Component({ | ||
30 | + selector: 'tb-security-settings', | ||
31 | + templateUrl: './security-settings.component.html', | ||
32 | + styleUrls: ['./security-settings.component.scss', './settings-card.scss'] | ||
33 | +}) | ||
34 | +export class SecuritySettingsComponent extends PageComponent implements OnInit, HasConfirmForm { | ||
35 | + | ||
36 | + securitySettingsFormGroup: FormGroup; | ||
37 | + securitySettings: SecuritySettings; | ||
38 | + | ||
39 | + constructor(protected store: Store<AppState>, | ||
40 | + private router: Router, | ||
41 | + private adminService: AdminService, | ||
42 | + private translate: TranslateService, | ||
43 | + public fb: FormBuilder) { | ||
44 | + super(store); | ||
45 | + } | ||
46 | + | ||
47 | + ngOnInit() { | ||
48 | + this.buildSecuritySettingsForm(); | ||
49 | + this.adminService.getSecuritySettings().subscribe( | ||
50 | + (securitySettings) => { | ||
51 | + this.securitySettings = securitySettings; | ||
52 | + this.securitySettingsFormGroup.reset(this.securitySettings); | ||
53 | + } | ||
54 | + ); | ||
55 | + } | ||
56 | + | ||
57 | + buildSecuritySettingsForm() { | ||
58 | + this.securitySettingsFormGroup = this.fb.group({ | ||
59 | + passwordPolicy: this.fb.group( | ||
60 | + { | ||
61 | + minimumLength: [null, [Validators.required, Validators.min(5), Validators.max(50)]], | ||
62 | + minimumUppercaseLetters: [null, Validators.min(0)], | ||
63 | + minimumLowercaseLetters: [null, Validators.min(0)], | ||
64 | + minimumDigits: [null, Validators.min(0)], | ||
65 | + minimumSpecialCharacters: [null, Validators.min(0)], | ||
66 | + passwordExpirationPeriodDays: [null, Validators.min(0)] | ||
67 | + } | ||
68 | + ) | ||
69 | + }); | ||
70 | + } | ||
71 | + | ||
72 | + save(): void { | ||
73 | + this.securitySettings = {...this.securitySettings, ...this.securitySettingsFormGroup.value}; | ||
74 | + this.adminService.saveSecuritySettings(this.securitySettings).subscribe( | ||
75 | + (securitySettings) => { | ||
76 | + this.securitySettings = securitySettings; | ||
77 | + this.securitySettingsFormGroup.reset(this.securitySettings); | ||
78 | + } | ||
79 | + ); | ||
80 | + } | ||
81 | + | ||
82 | + confirmForm(): FormGroup { | ||
83 | + return this.securitySettingsFormGroup; | ||
84 | + } | ||
85 | + | ||
86 | +} |
1 | +/** | ||
2 | + * Copyright © 2016-2019 The Thingsboard Authors | ||
3 | + * | ||
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | + * you may not use this file except in compliance with the License. | ||
6 | + * You may obtain a copy of the License at | ||
7 | + * | ||
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | + * | ||
10 | + * Unless required by applicable law or agreed to in writing, software | ||
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | + * See the License for the specific language governing permissions and | ||
14 | + * limitations under the License. | ||
15 | + */ | ||
16 | +@import "../../../../../scss/constants"; | ||
17 | + | ||
18 | +:host { | ||
19 | + mat-card.settings-card { | ||
20 | + margin: 8px; | ||
21 | + @media #{$mat-gt-sm} { | ||
22 | + width: 60%; | ||
23 | + } | ||
24 | + } | ||
25 | +} |
@@ -16,21 +16,23 @@ | @@ -16,21 +16,23 @@ | ||
16 | 16 | ||
17 | import { NgModule } from '@angular/core'; | 17 | import { NgModule } from '@angular/core'; |
18 | 18 | ||
19 | -// import { AdminModule } from './admin/admin.module'; | 19 | +import { AdminModule } from './admin/admin.module'; |
20 | import { HomeLinksModule } from './home-links/home-links.module'; | 20 | import { HomeLinksModule } from './home-links/home-links.module'; |
21 | -// import { ProfileModule } from './profile/profile.module'; | 21 | +import { ProfileModule } from './profile/profile.module'; |
22 | +import { TenantModule } from '@modules/home/pages/tenant/tenant.module'; | ||
22 | // import { CustomerModule } from '@modules/home/pages/customer/customer.module'; | 23 | // import { CustomerModule } from '@modules/home/pages/customer/customer.module'; |
23 | // import { AuditLogModule } from '@modules/home/pages/audit-log/audit-log.module'; | 24 | // import { AuditLogModule } from '@modules/home/pages/audit-log/audit-log.module'; |
24 | -// import { UserModule } from '@modules/home/pages/user/user.module'; | 25 | +import { UserModule } from '@modules/home/pages/user/user.module'; |
25 | 26 | ||
26 | @NgModule({ | 27 | @NgModule({ |
27 | exports: [ | 28 | exports: [ |
28 | -// AdminModule, | 29 | + AdminModule, |
29 | HomeLinksModule, | 30 | HomeLinksModule, |
30 | -// ProfileModule, | 31 | + ProfileModule, |
32 | + TenantModule, | ||
31 | // CustomerModule, | 33 | // CustomerModule, |
32 | // AuditLogModule, | 34 | // AuditLogModule, |
33 | -// UserModule | 35 | + UserModule |
34 | ] | 36 | ] |
35 | }) | 37 | }) |
36 | export class HomePagesModule { } | 38 | export class HomePagesModule { } |
1 | +<!-- | ||
2 | + | ||
3 | + Copyright © 2016-2019 The Thingsboard Authors | ||
4 | + | ||
5 | + Licensed under the Apache License, Version 2.0 (the "License"); | ||
6 | + you may not use this file except in compliance with the License. | ||
7 | + You may obtain a copy of the License at | ||
8 | + | ||
9 | + http://www.apache.org/licenses/LICENSE-2.0 | ||
10 | + | ||
11 | + Unless required by applicable law or agreed to in writing, software | ||
12 | + distributed under the License is distributed on an "AS IS" BASIS, | ||
13 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
14 | + See the License for the specific language governing permissions and | ||
15 | + limitations under the License. | ||
16 | + | ||
17 | +--> | ||
18 | +<form #changePasswordForm="ngForm" [formGroup]="changePassword" (ngSubmit)="onChangePassword()"> | ||
19 | + <mat-toolbar fxLayout="row" color="primary"> | ||
20 | + <h2 translate>profile.change-password</h2> | ||
21 | + <span fxFlex></span> | ||
22 | + <button mat-button mat-icon-button | ||
23 | + [mat-dialog-close]="false" | ||
24 | + type="button"> | ||
25 | + <mat-icon class="material-icons">close</mat-icon> | ||
26 | + </button> | ||
27 | + </mat-toolbar> | ||
28 | + <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> | ||
29 | + </mat-progress-bar> | ||
30 | + <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div> | ||
31 | + <div mat-dialog-content> | ||
32 | + <mat-form-field class="mat-block"> | ||
33 | + <mat-label translate>profile.current-password</mat-label> | ||
34 | + <input matInput type="password" formControlName="currentPassword"/> | ||
35 | + <mat-icon class="material-icons" matPrefix>lock</mat-icon> | ||
36 | + </mat-form-field> | ||
37 | + <mat-form-field class="mat-block"> | ||
38 | + <mat-label translate>login.new-password</mat-label> | ||
39 | + <input matInput type="password" formControlName="newPassword"/> | ||
40 | + <mat-icon class="material-icons" matPrefix>lock</mat-icon> | ||
41 | + </mat-form-field> | ||
42 | + <mat-form-field class="mat-block"> | ||
43 | + <mat-label translate>login.new-password-again</mat-label> | ||
44 | + <input matInput type="password" formControlName="newPassword2"/> | ||
45 | + <mat-icon class="material-icons" matPrefix>lock</mat-icon> | ||
46 | + </mat-form-field> | ||
47 | + </div> | ||
48 | + <div mat-dialog-actions fxLayout="row"> | ||
49 | + <span fxFlex></span> | ||
50 | + <button mat-button mat-raised-button color="primary" | ||
51 | + type="submit" | ||
52 | + [disabled]="(isLoading$ | async) || changePasswordForm.invalid"> | ||
53 | + {{ 'profile.change-password' | translate }} | ||
54 | + </button> | ||
55 | + <button mat-button color="primary" | ||
56 | + style="margin-right: 20px;" | ||
57 | + type="button" | ||
58 | + [disabled]="(isLoading$ | async)" | ||
59 | + [mat-dialog-close]="false" cdkFocusInitial> | ||
60 | + {{ 'action.cancel' | translate }} | ||
61 | + </button> | ||
62 | + </div> | ||
63 | +</form> |
1 | +/** | ||
2 | + * Copyright © 2016-2019 The Thingsboard Authors | ||
3 | + * | ||
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | + * you may not use this file except in compliance with the License. | ||
6 | + * You may obtain a copy of the License at | ||
7 | + * | ||
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | + * | ||
10 | + * Unless required by applicable law or agreed to in writing, software | ||
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | + * See the License for the specific language governing permissions and | ||
14 | + * limitations under the License. | ||
15 | + */ | ||
16 | +:host { | ||
17 | +} |
1 | +/// | ||
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | ||
3 | +/// | ||
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | +/// you may not use this file except in compliance with the License. | ||
6 | +/// You may obtain a copy of the License at | ||
7 | +/// | ||
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | +/// | ||
10 | +/// Unless required by applicable law or agreed to in writing, software | ||
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | +/// See the License for the specific language governing permissions and | ||
14 | +/// limitations under the License. | ||
15 | +/// | ||
16 | + | ||
17 | +import { Component, OnInit } from '@angular/core'; | ||
18 | +import { MatDialogRef } from '@angular/material'; | ||
19 | +import { PageComponent } from '@shared/components/page.component'; | ||
20 | +import { Store } from '@ngrx/store'; | ||
21 | +import { AppState } from '@core/core.state'; | ||
22 | +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; | ||
23 | +import { ActionNotificationShow } from '@core/notification/notification.actions'; | ||
24 | +import { TranslateService } from '@ngx-translate/core'; | ||
25 | +import { AuthService } from '@core/auth/auth.service'; | ||
26 | + | ||
27 | +@Component({ | ||
28 | + selector: 'tb-change-password-dialog', | ||
29 | + templateUrl: './change-password-dialog.component.html', | ||
30 | + styleUrls: ['./change-password-dialog.component.scss'] | ||
31 | +}) | ||
32 | +export class ChangePasswordDialogComponent extends PageComponent implements OnInit { | ||
33 | + | ||
34 | + changePassword: FormGroup; | ||
35 | + | ||
36 | + constructor(protected store: Store<AppState>, | ||
37 | + private translate: TranslateService, | ||
38 | + private authService: AuthService, | ||
39 | + public dialogRef: MatDialogRef<ChangePasswordDialogComponent>, | ||
40 | + public fb: FormBuilder) { | ||
41 | + super(store); | ||
42 | + } | ||
43 | + | ||
44 | + ngOnInit(): void { | ||
45 | + this.buildChangePasswordForm(); | ||
46 | + } | ||
47 | + | ||
48 | + buildChangePasswordForm() { | ||
49 | + this.changePassword = this.fb.group({ | ||
50 | + currentPassword: [''], | ||
51 | + newPassword: [''], | ||
52 | + newPassword2: [''] | ||
53 | + }); | ||
54 | + } | ||
55 | + | ||
56 | + onChangePassword(): void { | ||
57 | + if (this.changePassword.get('newPassword').value !== this.changePassword.get('newPassword2').value) { | ||
58 | + this.store.dispatch(new ActionNotificationShow({ message: this.translate.instant('login.passwords-mismatch-error'), | ||
59 | + type: 'error' })); | ||
60 | + } else { | ||
61 | + this.authService.changePassword( | ||
62 | + this.changePassword.get('currentPassword').value, | ||
63 | + this.changePassword.get('newPassword').value).subscribe(() => { | ||
64 | + this.dialogRef.close(true); | ||
65 | + }); | ||
66 | + } | ||
67 | + } | ||
68 | +} |
1 | +/// | ||
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | ||
3 | +/// | ||
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | +/// you may not use this file except in compliance with the License. | ||
6 | +/// You may obtain a copy of the License at | ||
7 | +/// | ||
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | +/// | ||
10 | +/// Unless required by applicable law or agreed to in writing, software | ||
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | +/// See the License for the specific language governing permissions and | ||
14 | +/// limitations under the License. | ||
15 | +/// | ||
16 | + | ||
17 | +import {Injectable, NgModule} from '@angular/core'; | ||
18 | +import {Resolve, RouterModule, Routes} from '@angular/router'; | ||
19 | + | ||
20 | +import {ProfileComponent} from './profile.component'; | ||
21 | +import {ConfirmOnExitGuard} from '@core/guards/confirm-on-exit.guard'; | ||
22 | +import {Authority} from '@shared/models/authority.enum'; | ||
23 | +import {User} from '@shared/models/user.model'; | ||
24 | +import {Store} from '@ngrx/store'; | ||
25 | +import {AppState} from '@core/core.state'; | ||
26 | +import {UserService} from '@core/http/user.service'; | ||
27 | +import {getCurrentAuthUser} from '@core/auth/auth.selectors'; | ||
28 | +import {Observable} from 'rxjs'; | ||
29 | + | ||
30 | +@Injectable() | ||
31 | +export class UserProfileResolver implements Resolve<User> { | ||
32 | + | ||
33 | + constructor(private store: Store<AppState>, | ||
34 | + private userService: UserService) { | ||
35 | + } | ||
36 | + | ||
37 | + resolve(): Observable<User> { | ||
38 | + const userId = getCurrentAuthUser(this.store).userId; | ||
39 | + return this.userService.getUser(userId); | ||
40 | + } | ||
41 | +} | ||
42 | + | ||
43 | +const routes: Routes = [ | ||
44 | + { | ||
45 | + path: 'profile', | ||
46 | + component: ProfileComponent, | ||
47 | + canDeactivate: [ConfirmOnExitGuard], | ||
48 | + data: { | ||
49 | + auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN, Authority.CUSTOMER_USER], | ||
50 | + title: 'profile.profile', | ||
51 | + breadcrumb: { | ||
52 | + label: 'profile.profile', | ||
53 | + icon: 'account_circle' | ||
54 | + } | ||
55 | + }, | ||
56 | + resolve: { | ||
57 | + user: UserProfileResolver | ||
58 | + } | ||
59 | + } | ||
60 | +]; | ||
61 | + | ||
62 | +@NgModule({ | ||
63 | + imports: [RouterModule.forChild(routes)], | ||
64 | + exports: [RouterModule], | ||
65 | + providers: [ | ||
66 | + UserProfileResolver | ||
67 | + ] | ||
68 | +}) | ||
69 | +export class ProfileRoutingModule { } |
1 | +<!-- | ||
2 | + | ||
3 | + Copyright © 2016-2019 The Thingsboard Authors | ||
4 | + | ||
5 | + Licensed under the Apache License, Version 2.0 (the "License"); | ||
6 | + you may not use this file except in compliance with the License. | ||
7 | + You may obtain a copy of the License at | ||
8 | + | ||
9 | + http://www.apache.org/licenses/LICENSE-2.0 | ||
10 | + | ||
11 | + Unless required by applicable law or agreed to in writing, software | ||
12 | + distributed under the License is distributed on an "AS IS" BASIS, | ||
13 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
14 | + See the License for the specific language governing permissions and | ||
15 | + limitations under the License. | ||
16 | + | ||
17 | +--> | ||
18 | +<div> | ||
19 | + <mat-card class="profile-card"> | ||
20 | + <mat-card-title> | ||
21 | + <div fxLayout="column"> | ||
22 | + <span class="mat-headline" translate>profile.profile</span> | ||
23 | + <span class="profile-email" style='opacity: 0.7;'>{{ profile ? profile.get('email').value : '' }}</span> | ||
24 | + </div> | ||
25 | + </mat-card-title> | ||
26 | + <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> | ||
27 | + </mat-progress-bar> | ||
28 | + <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div> | ||
29 | + <mat-card-content style="padding-top: 16px;"> | ||
30 | + <form #profileForm="ngForm" [formGroup]="profile" (ngSubmit)="save()"> | ||
31 | + <fieldset [disabled]="isLoading$ | async"> | ||
32 | + <mat-form-field class="mat-block"> | ||
33 | + <mat-label translate>user.email</mat-label> | ||
34 | + <input matInput formControlName="email" required/> | ||
35 | + <mat-error *ngIf="profile.get('email').hasError('required')"> | ||
36 | + {{ 'user.email-required' | translate }} | ||
37 | + </mat-error> | ||
38 | + <mat-error *ngIf="profile.get('email').hasError('email')"> | ||
39 | + {{ 'user.invalid-email-format' | translate }} | ||
40 | + </mat-error> | ||
41 | + </mat-form-field> | ||
42 | + <mat-form-field class="mat-block"> | ||
43 | + <mat-label translate>user.first-name</mat-label> | ||
44 | + <input matInput formControlName="firstName"/> | ||
45 | + </mat-form-field> | ||
46 | + <mat-form-field class="mat-block"> | ||
47 | + <mat-label translate>user.last-name</mat-label> | ||
48 | + <input matInput formControlName="lastName"/> | ||
49 | + </mat-form-field> | ||
50 | + <mat-form-field class="mat-block"> | ||
51 | + <mat-label translate>language.language</mat-label> | ||
52 | + <mat-select matInput formControlName="language"> | ||
53 | + <mat-option *ngFor="let lang of languageList" [value]="lang"> | ||
54 | + {{ lang ? ('language.locales.' + lang | translate) : ''}} | ||
55 | + </mat-option> | ||
56 | + </mat-select> | ||
57 | + </mat-form-field> | ||
58 | + <div fxLayout="row" style="padding-bottom: 16px;"> | ||
59 | + <button mat-button mat-raised-button color="primary" | ||
60 | + type="button" | ||
61 | + [disabled]="(isLoading$ | async)" (click)="changePassword()"> | ||
62 | + {{'profile.change-password' | translate}} | ||
63 | + </button> | ||
64 | + </div> | ||
65 | + <div fxLayout="row" class="layout-wrap"> | ||
66 | + <span fxFlex></span> | ||
67 | + <button mat-button mat-raised-button color="primary" | ||
68 | + type="submit" | ||
69 | + [disabled]="(isLoading$ | async) || profileForm.invalid || !profileForm.dirty"> | ||
70 | + {{ 'action.save' | translate }} | ||
71 | + </button> | ||
72 | + </div> | ||
73 | + </fieldset> | ||
74 | + </form> | ||
75 | + </mat-card-content> | ||
76 | + </mat-card> | ||
77 | +</div> |
1 | +/** | ||
2 | + * Copyright © 2016-2019 The Thingsboard Authors | ||
3 | + * | ||
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | + * you may not use this file except in compliance with the License. | ||
6 | + * You may obtain a copy of the License at | ||
7 | + * | ||
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | + * | ||
10 | + * Unless required by applicable law or agreed to in writing, software | ||
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | + * See the License for the specific language governing permissions and | ||
14 | + * limitations under the License. | ||
15 | + */ | ||
16 | +@import "../../../../../scss/constants"; | ||
17 | + | ||
18 | +:host { | ||
19 | + mat-card.profile-card { | ||
20 | + margin: 8px; | ||
21 | + @media #{$mat-gt-sm} { | ||
22 | + width: 60%; | ||
23 | + } | ||
24 | + .mat-headline { | ||
25 | + margin: 0; | ||
26 | + } | ||
27 | + .profile-email { | ||
28 | + font-size: 16px; | ||
29 | + font-weight: 400; | ||
30 | + } | ||
31 | + } | ||
32 | +} |
1 | +/// | ||
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | ||
3 | +/// | ||
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | +/// you may not use this file except in compliance with the License. | ||
6 | +/// You may obtain a copy of the License at | ||
7 | +/// | ||
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | +/// | ||
10 | +/// Unless required by applicable law or agreed to in writing, software | ||
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | +/// See the License for the specific language governing permissions and | ||
14 | +/// limitations under the License. | ||
15 | +/// | ||
16 | + | ||
17 | +import { Component, OnInit } from '@angular/core'; | ||
18 | +import { UserService } from '@core/http/user.service'; | ||
19 | +import { User } from '@shared/models/user.model'; | ||
20 | +import { Authority } from '@shared/models/authority.enum'; | ||
21 | +import { PageComponent } from '@shared/components/page.component'; | ||
22 | +import { select, Store } from '@ngrx/store'; | ||
23 | +import { AppState } from '@core/core.state'; | ||
24 | +import { getCurrentAuthUser, selectAuthUser } from '@core/auth/auth.selectors'; | ||
25 | +import { mergeMap, take } from 'rxjs/operators'; | ||
26 | +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; | ||
27 | +import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard'; | ||
28 | +import { ActionAuthUpdateUserDetails } from '@core/auth/auth.actions'; | ||
29 | +import { environment as env } from '@env/environment'; | ||
30 | +import { TranslateService } from '@ngx-translate/core'; | ||
31 | +import { ActionSettingsChangeLanguage } from '@core/settings/settings.actions'; | ||
32 | +import { ChangePasswordDialogComponent } from '@modules/home/pages/profile/change-password-dialog.component'; | ||
33 | +import { MatDialog } from '@angular/material'; | ||
34 | +import { DialogService } from '@core/services/dialog.service'; | ||
35 | +import { AuthService } from '@core/auth/auth.service'; | ||
36 | +import { ActivatedRoute } from '@angular/router'; | ||
37 | + | ||
38 | +@Component({ | ||
39 | + selector: 'tb-profile', | ||
40 | + templateUrl: './profile.component.html', | ||
41 | + styleUrls: ['./profile.component.scss'] | ||
42 | +}) | ||
43 | +export class ProfileComponent extends PageComponent implements OnInit, HasConfirmForm { | ||
44 | + | ||
45 | + authorities = Authority; | ||
46 | + profile: FormGroup; | ||
47 | + user: User; | ||
48 | + languageList = env.supportedLangs; | ||
49 | + | ||
50 | + constructor(protected store: Store<AppState>, | ||
51 | + private route: ActivatedRoute, | ||
52 | + private userService: UserService, | ||
53 | + private authService: AuthService, | ||
54 | + private translate: TranslateService, | ||
55 | + public dialog: MatDialog, | ||
56 | + public dialogService: DialogService, | ||
57 | + public fb: FormBuilder) { | ||
58 | + super(store); | ||
59 | + } | ||
60 | + | ||
61 | + ngOnInit() { | ||
62 | + this.buildProfileForm(); | ||
63 | + this.userLoaded(this.route.snapshot.data.user); | ||
64 | + } | ||
65 | + | ||
66 | + buildProfileForm() { | ||
67 | + this.profile = this.fb.group({ | ||
68 | + email: ['', [Validators.required, Validators.email]], | ||
69 | + firstName: [''], | ||
70 | + lastName: [''], | ||
71 | + language: [''] | ||
72 | + }); | ||
73 | + } | ||
74 | + | ||
75 | + save(): void { | ||
76 | + this.user = {...this.user, ...this.profile.value}; | ||
77 | + if (!this.user.additionalInfo) { | ||
78 | + this.user.additionalInfo = {}; | ||
79 | + } | ||
80 | + this.user.additionalInfo.lang = this.profile.get('language').value; | ||
81 | + this.userService.saveUser(this.user).subscribe( | ||
82 | + (user) => { | ||
83 | + this.userLoaded(user); | ||
84 | + this.store.dispatch(new ActionAuthUpdateUserDetails({ userDetails: { | ||
85 | + additionalInfo: {...user.additionalInfo}, | ||
86 | + authority: user.authority, | ||
87 | + createdTime: user.createdTime, | ||
88 | + tenantId: user.tenantId, | ||
89 | + customerId: user.customerId, | ||
90 | + email: user.email, | ||
91 | + firstName: user.firstName, | ||
92 | + id: user.id, | ||
93 | + lastName: user.lastName, | ||
94 | + } })); | ||
95 | + this.store.dispatch(new ActionSettingsChangeLanguage({ userLang: user.additionalInfo.lang })); | ||
96 | + } | ||
97 | + ); | ||
98 | + } | ||
99 | + | ||
100 | + changePassword(): void { | ||
101 | + this.dialog.open(ChangePasswordDialogComponent, { | ||
102 | + disableClose: true, | ||
103 | + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'] | ||
104 | + }); | ||
105 | + } | ||
106 | + | ||
107 | + userLoaded(user: User) { | ||
108 | + this.user = user; | ||
109 | + this.profile.reset(user); | ||
110 | + let lang; | ||
111 | + if (user.additionalInfo && user.additionalInfo.lang) { | ||
112 | + lang = user.additionalInfo.lang; | ||
113 | + } else { | ||
114 | + lang = this.translate.currentLang; | ||
115 | + } | ||
116 | + this.profile.get('language').setValue(lang); | ||
117 | + } | ||
118 | + | ||
119 | + confirmForm(): FormGroup { | ||
120 | + return this.profile; | ||
121 | + } | ||
122 | + | ||
123 | +} |
1 | +/// | ||
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | ||
3 | +/// | ||
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | +/// you may not use this file except in compliance with the License. | ||
6 | +/// You may obtain a copy of the License at | ||
7 | +/// | ||
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | +/// | ||
10 | +/// Unless required by applicable law or agreed to in writing, software | ||
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | +/// See the License for the specific language governing permissions and | ||
14 | +/// limitations under the License. | ||
15 | +/// | ||
16 | + | ||
17 | +import { NgModule } from '@angular/core'; | ||
18 | +import { CommonModule } from '@angular/common'; | ||
19 | +import { ProfileComponent } from './profile.component'; | ||
20 | +import { SharedModule } from '@shared/shared.module'; | ||
21 | +import { ProfileRoutingModule } from './profile-routing.module'; | ||
22 | +import { ChangePasswordDialogComponent } from '@modules/home/pages/profile/change-password-dialog.component'; | ||
23 | + | ||
24 | +@NgModule({ | ||
25 | + entryComponents: [ | ||
26 | + ChangePasswordDialogComponent | ||
27 | + ], | ||
28 | + declarations: [ | ||
29 | + ProfileComponent, | ||
30 | + ChangePasswordDialogComponent | ||
31 | + ], | ||
32 | + imports: [ | ||
33 | + CommonModule, | ||
34 | + SharedModule, | ||
35 | + ProfileRoutingModule | ||
36 | + ] | ||
37 | +}) | ||
38 | +export class ProfileModule { } |
1 | +/// | ||
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | ||
3 | +/// | ||
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | +/// you may not use this file except in compliance with the License. | ||
6 | +/// You may obtain a copy of the License at | ||
7 | +/// | ||
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | +/// | ||
10 | +/// Unless required by applicable law or agreed to in writing, software | ||
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | +/// See the License for the specific language governing permissions and | ||
14 | +/// limitations under the License. | ||
15 | +/// | ||
16 | + | ||
17 | +import { Injectable, NgModule } from '@angular/core'; | ||
18 | +import { Resolve, RouterModule, Routes } from '@angular/router'; | ||
19 | + | ||
20 | +import { EntitiesTableComponent } from '@shared/components/entity/entities-table.component'; | ||
21 | +import { Authority } from '@shared/models/authority.enum'; | ||
22 | +import { TenantsTableConfigResolver } from '@modules/home/pages/tenant/tenants-table-config.resolver'; | ||
23 | +import { ProfileComponent } from '@modules/home/pages/profile/profile.component'; | ||
24 | +import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard'; | ||
25 | +import { Customer } from '@shared/models/customer.model'; | ||
26 | +import { Store } from '@ngrx/store'; | ||
27 | +import { AppState } from '@core/core.state'; | ||
28 | +import { forkJoin, Observable, throwError } from 'rxjs'; | ||
29 | +import { getCurrentAuthUser } from '@core/auth/auth.selectors'; | ||
30 | +import { catchError, finalize, map, tap } from 'rxjs/operators'; | ||
31 | +import {UsersTableConfigResolver} from '../user/users-table-config.resolver'; | ||
32 | + | ||
33 | +const routes: Routes = [ | ||
34 | + { | ||
35 | + path: 'tenants', | ||
36 | + data: { | ||
37 | + breadcrumb: { | ||
38 | + label: 'tenant.tenants', | ||
39 | + icon: 'supervisor_account' | ||
40 | + } | ||
41 | + }, | ||
42 | + children: [ | ||
43 | + { | ||
44 | + path: '', | ||
45 | + component: EntitiesTableComponent, | ||
46 | + data: { | ||
47 | + auth: [Authority.SYS_ADMIN], | ||
48 | + title: 'tenant.tenants' | ||
49 | + }, | ||
50 | + resolve: { | ||
51 | + entitiesTableConfig: TenantsTableConfigResolver | ||
52 | + } | ||
53 | + }, | ||
54 | + { | ||
55 | + path: ':tenantId/users', | ||
56 | + component: EntitiesTableComponent, | ||
57 | + data: { | ||
58 | + auth: [Authority.SYS_ADMIN], | ||
59 | + title: 'user.tenant-admins', | ||
60 | + breadcrumb: { | ||
61 | + label: 'user.tenant-admins', | ||
62 | + icon: 'account_circle' | ||
63 | + } | ||
64 | + }, | ||
65 | + resolve: { | ||
66 | + entitiesTableConfig: UsersTableConfigResolver | ||
67 | + } | ||
68 | + } | ||
69 | + ] | ||
70 | + } | ||
71 | +]; | ||
72 | + | ||
73 | +@NgModule({ | ||
74 | + imports: [RouterModule.forChild(routes)], | ||
75 | + exports: [RouterModule], | ||
76 | + providers: [ | ||
77 | + TenantsTableConfigResolver | ||
78 | + ] | ||
79 | +}) | ||
80 | +export class TenantRoutingModule { } |
1 | +<!-- | ||
2 | + | ||
3 | + Copyright © 2016-2019 The Thingsboard Authors | ||
4 | + | ||
5 | + Licensed under the Apache License, Version 2.0 (the "License"); | ||
6 | + you may not use this file except in compliance with the License. | ||
7 | + You may obtain a copy of the License at | ||
8 | + | ||
9 | + http://www.apache.org/licenses/LICENSE-2.0 | ||
10 | + | ||
11 | + Unless required by applicable law or agreed to in writing, software | ||
12 | + distributed under the License is distributed on an "AS IS" BASIS, | ||
13 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
14 | + See the License for the specific language governing permissions and | ||
15 | + limitations under the License. | ||
16 | + | ||
17 | +--> | ||
18 | +<div class="tb-details-buttons"> | ||
19 | + <button mat-raised-button color="primary" | ||
20 | + [disabled]="(isLoading$ | async)" | ||
21 | + (click)="onEntityAction($event, 'manageTenantAdmins')" | ||
22 | + [fxShow]="!isEdit"> | ||
23 | + {{'tenant.manage-tenant-admins' | translate }} | ||
24 | + </button> | ||
25 | + <button mat-raised-button color="primary" | ||
26 | + [disabled]="(isLoading$ | async)" | ||
27 | + (click)="onEntityAction($event, 'delete')" | ||
28 | + [fxShow]="!hideDelete() && !isEdit"> | ||
29 | + {{'tenant.delete' | translate }} | ||
30 | + </button> | ||
31 | + <div fxLayout="row"> | ||
32 | + <button mat-raised-button | ||
33 | + ngxClipboard | ||
34 | + (cbOnSuccess)="onTenantIdCopied($event)" | ||
35 | + [cbContent]="entity?.id?.id" | ||
36 | + [fxShow]="!isEdit"> | ||
37 | + <mat-icon svgIcon="mdi:clipboard-arrow-left"></mat-icon> | ||
38 | + <span translate>tenant.copyId</span> | ||
39 | + </button> | ||
40 | + </div> | ||
41 | +</div> | ||
42 | +<div class="mat-padding" fxLayout="column"> | ||
43 | + <form #entityNgForm="ngForm" [formGroup]="entityForm"> | ||
44 | + <fieldset [disabled]="(isLoading$ | async) || !isEdit"> | ||
45 | + <mat-form-field class="mat-block"> | ||
46 | + <mat-label translate>tenant.title</mat-label> | ||
47 | + <input matInput formControlName="title" required/> | ||
48 | + <mat-error *ngIf="entityForm.get('title').hasError('required')"> | ||
49 | + {{ 'tenant.title-required' | translate }} | ||
50 | + </mat-error> | ||
51 | + </mat-form-field> | ||
52 | + <div formGroupName="additionalInfo" fxLayout="column"> | ||
53 | + <mat-form-field class="mat-block"> | ||
54 | + <mat-label translate>tenant.description</mat-label> | ||
55 | + <textarea matInput formControlName="description" rows="2"></textarea> | ||
56 | + </mat-form-field> | ||
57 | + </div> | ||
58 | + <tb-contact [parentForm]="entityForm" [isEdit]="isEdit"></tb-contact> | ||
59 | + </fieldset> | ||
60 | + </form> | ||
61 | +</div> |
1 | +/// | ||
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | ||
3 | +/// | ||
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | +/// you may not use this file except in compliance with the License. | ||
6 | +/// You may obtain a copy of the License at | ||
7 | +/// | ||
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | +/// | ||
10 | +/// Unless required by applicable law or agreed to in writing, software | ||
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | +/// See the License for the specific language governing permissions and | ||
14 | +/// limitations under the License. | ||
15 | +/// | ||
16 | + | ||
17 | +import { Component } from '@angular/core'; | ||
18 | +import { Store } from '@ngrx/store'; | ||
19 | +import { AppState } from '@core/core.state'; | ||
20 | +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; | ||
21 | +import { Customer } from '@shared/models/customer.model'; | ||
22 | +import { ContactBasedComponent } from '@shared/components/entity/contact-based.component'; | ||
23 | +import {Tenant} from '@app/shared/models/tenant.model'; | ||
24 | +import {ActionNotificationShow} from '@app/core/notification/notification.actions'; | ||
25 | +import {TranslateService} from '@ngx-translate/core'; | ||
26 | + | ||
27 | +@Component({ | ||
28 | + selector: 'tb-tenant', | ||
29 | + templateUrl: './tenant.component.html' | ||
30 | +}) | ||
31 | +export class TenantComponent extends ContactBasedComponent<Tenant> { | ||
32 | + | ||
33 | + constructor(protected store: Store<AppState>, | ||
34 | + protected translate: TranslateService, | ||
35 | + protected fb: FormBuilder) { | ||
36 | + super(store, fb); | ||
37 | + } | ||
38 | + | ||
39 | + hideDelete() { | ||
40 | + if (this.entitiesTableConfig) { | ||
41 | + return !this.entitiesTableConfig.deleteEnabled(this.entity); | ||
42 | + } else { | ||
43 | + return false; | ||
44 | + } | ||
45 | + } | ||
46 | + | ||
47 | + buildEntityForm(entity: Tenant): FormGroup { | ||
48 | + return this.fb.group( | ||
49 | + { | ||
50 | + title: [entity ? entity.title : '', [Validators.required]], | ||
51 | + additionalInfo: this.fb.group( | ||
52 | + { | ||
53 | + description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''] | ||
54 | + } | ||
55 | + ) | ||
56 | + } | ||
57 | + ); | ||
58 | + } | ||
59 | + | ||
60 | + updateEntityForm(entity: Tenant) { | ||
61 | + this.entityForm.patchValue({title: entity.title}); | ||
62 | + this.entityForm.patchValue({additionalInfo: {description: entity.additionalInfo ? entity.additionalInfo.description : ''}}); | ||
63 | + } | ||
64 | + | ||
65 | + onTenantIdCopied(event) { | ||
66 | + this.store.dispatch(new ActionNotificationShow( | ||
67 | + { | ||
68 | + message: this.translate.instant('tenant.idCopiedMessage'), | ||
69 | + type: 'success', | ||
70 | + duration: 750, | ||
71 | + verticalPosition: 'bottom', | ||
72 | + horizontalPosition: 'right' | ||
73 | + })); | ||
74 | + } | ||
75 | + | ||
76 | +} |
1 | +/// | ||
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | ||
3 | +/// | ||
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | +/// you may not use this file except in compliance with the License. | ||
6 | +/// You may obtain a copy of the License at | ||
7 | +/// | ||
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | +/// | ||
10 | +/// Unless required by applicable law or agreed to in writing, software | ||
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | +/// See the License for the specific language governing permissions and | ||
14 | +/// limitations under the License. | ||
15 | +/// | ||
16 | + | ||
17 | +import { NgModule } from '@angular/core'; | ||
18 | +import { CommonModule } from '@angular/common'; | ||
19 | +import { SharedModule } from '@shared/shared.module'; | ||
20 | +import {TenantComponent} from '@modules/home/pages/tenant/tenant.component'; | ||
21 | +import {TenantRoutingModule} from '@modules/home/pages/tenant/tenant-routing.module'; | ||
22 | + | ||
23 | +@NgModule({ | ||
24 | + entryComponents: [ | ||
25 | + TenantComponent | ||
26 | + ], | ||
27 | + declarations: [ | ||
28 | + TenantComponent | ||
29 | + ], | ||
30 | + imports: [ | ||
31 | + CommonModule, | ||
32 | + SharedModule, | ||
33 | + TenantRoutingModule | ||
34 | + ] | ||
35 | +}) | ||
36 | +export class TenantModule { } |
1 | +/// | ||
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | ||
3 | +/// | ||
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | +/// you may not use this file except in compliance with the License. | ||
6 | +/// You may obtain a copy of the License at | ||
7 | +/// | ||
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | +/// | ||
10 | +/// Unless required by applicable law or agreed to in writing, software | ||
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | +/// See the License for the specific language governing permissions and | ||
14 | +/// limitations under the License. | ||
15 | +/// | ||
16 | + | ||
17 | +import { Injectable } from '@angular/core'; | ||
18 | + | ||
19 | +import { Resolve, Router } from '@angular/router'; | ||
20 | + | ||
21 | +import { Tenant } from '@shared/models/tenant.model'; | ||
22 | +import { | ||
23 | + DateEntityTableColumn, | ||
24 | + EntityTableColumn, | ||
25 | + EntityTableConfig | ||
26 | +} from '@shared/components/entity/entities-table-config.models'; | ||
27 | +import { TenantService } from '@core/http/tenant.service'; | ||
28 | +import { TranslateService } from '@ngx-translate/core'; | ||
29 | +import { DatePipe } from '@angular/common'; | ||
30 | +import { | ||
31 | + EntityType, | ||
32 | + entityTypeResources, | ||
33 | + entityTypeTranslations | ||
34 | +} from '@shared/models/entity-type.models'; | ||
35 | +import { TenantComponent } from '@modules/home/pages/tenant/tenant.component'; | ||
36 | +import { EntityAction } from '@shared/components/entity/entity-component.models'; | ||
37 | +import { User } from '@shared/models/user.model'; | ||
38 | + | ||
39 | +@Injectable() | ||
40 | +export class TenantsTableConfigResolver implements Resolve<EntityTableConfig<Tenant>> { | ||
41 | + | ||
42 | + private readonly config: EntityTableConfig<Tenant> = new EntityTableConfig<Tenant>(); | ||
43 | + | ||
44 | + constructor(private tenantService: TenantService, | ||
45 | + private translate: TranslateService, | ||
46 | + private datePipe: DatePipe, | ||
47 | + private router: Router) { | ||
48 | + | ||
49 | + this.config.entityType = EntityType.CUSTOMER; | ||
50 | + this.config.entityComponent = TenantComponent; | ||
51 | + this.config.entityTranslations = entityTypeTranslations.get(EntityType.TENANT); | ||
52 | + this.config.entityResources = entityTypeResources.get(EntityType.TENANT); | ||
53 | + | ||
54 | + this.config.columns.push( | ||
55 | + new DateEntityTableColumn<Tenant>('createdTime', 'tenant.created-time', this.datePipe, '150px'), | ||
56 | + new EntityTableColumn<Tenant>('title', 'tenant.title'), | ||
57 | + new EntityTableColumn<Tenant>('email', 'contact.email'), | ||
58 | + new EntityTableColumn<Tenant>('country', 'contact.country'), | ||
59 | + new EntityTableColumn<Tenant>('city', 'contact.city') | ||
60 | + ); | ||
61 | + | ||
62 | + this.config.cellActionDescriptors.push( | ||
63 | + { | ||
64 | + name: this.translate.instant('tenant.manage-tenant-admins'), | ||
65 | + icon: 'account_circle', | ||
66 | + isEnabled: () => true, | ||
67 | + onAction: ($event, entity) => this.manageTenantAdmins($event, entity) | ||
68 | + } | ||
69 | + ); | ||
70 | + | ||
71 | + this.config.deleteEntityTitle = tenant => this.translate.instant('tenant.delete-tenant-title', { tenantTitle: tenant.title }); | ||
72 | + this.config.deleteEntityContent = () => this.translate.instant('tenant.delete-tenant-text'); | ||
73 | + this.config.deleteEntitiesTitle = count => this.translate.instant('tenant.delete-tenants-title', {count}); | ||
74 | + this.config.deleteEntitiesContent = () => this.translate.instant('tenant.delete-tenants-text'); | ||
75 | + | ||
76 | + this.config.entitiesFetchFunction = pageLink => this.tenantService.getTenants(pageLink); | ||
77 | + this.config.loadEntity = id => this.tenantService.getTenant(id.id); | ||
78 | + this.config.saveEntity = tenant => this.tenantService.saveTenant(tenant); | ||
79 | + this.config.deleteEntity = id => this.tenantService.deleteTenant(id.id); | ||
80 | + this.config.onEntityAction = action => this.onTenantAction(action); | ||
81 | + } | ||
82 | + | ||
83 | + resolve(): EntityTableConfig<Tenant> { | ||
84 | + this.config.tableTitle = this.translate.instant('tenant.tenants'); | ||
85 | + | ||
86 | + return this.config; | ||
87 | + } | ||
88 | + | ||
89 | + manageTenantAdmins($event: Event, tenant: Tenant) { | ||
90 | + if ($event) { | ||
91 | + $event.stopPropagation(); | ||
92 | + } | ||
93 | + this.router.navigateByUrl(`tenants/${tenant.id.id}/users`); | ||
94 | + } | ||
95 | + | ||
96 | + onTenantAction(action: EntityAction<Tenant>): boolean { | ||
97 | + switch (action.action) { | ||
98 | + case 'manageTenantAdmins': | ||
99 | + this.manageTenantAdmins(action.event, action.entity); | ||
100 | + return true; | ||
101 | + } | ||
102 | + return false; | ||
103 | + } | ||
104 | + | ||
105 | +} |
1 | +<!-- | ||
2 | + | ||
3 | + Copyright © 2016-2019 The Thingsboard Authors | ||
4 | + | ||
5 | + Licensed under the Apache License, Version 2.0 (the "License"); | ||
6 | + you may not use this file except in compliance with the License. | ||
7 | + You may obtain a copy of the License at | ||
8 | + | ||
9 | + http://www.apache.org/licenses/LICENSE-2.0 | ||
10 | + | ||
11 | + Unless required by applicable law or agreed to in writing, software | ||
12 | + distributed under the License is distributed on an "AS IS" BASIS, | ||
13 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
14 | + See the License for the specific language governing permissions and | ||
15 | + limitations under the License. | ||
16 | + | ||
17 | +--> | ||
18 | +<form style="min-width: 400px;"> | ||
19 | + <mat-toolbar fxLayout="row" color="primary"> | ||
20 | + <h2 translate>user.activation-link</h2> | ||
21 | + <span fxFlex></span> | ||
22 | + <button mat-button mat-icon-button | ||
23 | + (click)="close()" | ||
24 | + type="button"> | ||
25 | + <mat-icon class="material-icons">close</mat-icon> | ||
26 | + </button> | ||
27 | + </mat-toolbar> | ||
28 | + <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> | ||
29 | + </mat-progress-bar> | ||
30 | + <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div> | ||
31 | + <div mat-dialog-content tb-toast toastTarget="activationLinkDialogContent"> | ||
32 | + <div class="mat-content mat-padding" fxLayout="column"> | ||
33 | + <span [innerHTML]="'user.activation-link-text' | translate: {activationLink: activationLink}"></span> | ||
34 | + <div fxLayout="row" fxLayoutAlign="start center"> | ||
35 | + <pre class="tb-highlight" fxFlex><code>{{ activationLink }}</code></pre> | ||
36 | + <button mat-button mat-icon-button | ||
37 | + color="primary" | ||
38 | + ngxClipboard | ||
39 | + cbContent="{{ activationLink }}" | ||
40 | + (cbOnSuccess)="onActivationLinkCopied()" | ||
41 | + matTooltip="{{ 'user.copy-activation-link' | translate }}" | ||
42 | + matTooltipPosition="above"> | ||
43 | + <mat-icon svgIcon="mdi:clipboard-arrow-left"></mat-icon> | ||
44 | + </button> | ||
45 | + </div> | ||
46 | + </div> | ||
47 | + </div> | ||
48 | + <div mat-dialog-actions fxLayout="row"> | ||
49 | + <span fxFlex></span> | ||
50 | + <button mat-button color="primary" | ||
51 | + style="margin-right: 20px;" | ||
52 | + type="button" | ||
53 | + cdkFocusInitial | ||
54 | + [disabled]="(isLoading$ | async)" | ||
55 | + (click)="close()"> | ||
56 | + {{ 'action.ok' | translate }} | ||
57 | + </button> | ||
58 | + </div> | ||
59 | +</form> |
1 | +/// | ||
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | ||
3 | +/// | ||
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | +/// you may not use this file except in compliance with the License. | ||
6 | +/// You may obtain a copy of the License at | ||
7 | +/// | ||
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | +/// | ||
10 | +/// Unless required by applicable law or agreed to in writing, software | ||
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | +/// See the License for the specific language governing permissions and | ||
14 | +/// limitations under the License. | ||
15 | +/// | ||
16 | + | ||
17 | +import { Component, Inject, OnInit } from '@angular/core'; | ||
18 | +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material'; | ||
19 | +import { PageComponent } from '@shared/components/page.component'; | ||
20 | +import { Store } from '@ngrx/store'; | ||
21 | +import { AppState } from '@core/core.state'; | ||
22 | +import { TranslateService } from '@ngx-translate/core'; | ||
23 | +import { ActionNotificationShow } from '@core/notification/notification.actions'; | ||
24 | + | ||
25 | +export interface ActivationLinkDialogData { | ||
26 | + activationLink: string; | ||
27 | +} | ||
28 | + | ||
29 | +@Component({ | ||
30 | + selector: 'tb-activation-link-dialog', | ||
31 | + templateUrl: './activation-link-dialog.component.html' | ||
32 | +}) | ||
33 | +export class ActivationLinkDialogComponent extends PageComponent implements OnInit { | ||
34 | + | ||
35 | + activationLink: string; | ||
36 | + | ||
37 | + constructor(protected store: Store<AppState>, | ||
38 | + @Inject(MAT_DIALOG_DATA) public data: ActivationLinkDialogData, | ||
39 | + public dialogRef: MatDialogRef<ActivationLinkDialogComponent, void>, | ||
40 | + private translate: TranslateService) { | ||
41 | + super(store); | ||
42 | + this.activationLink = this.data.activationLink; | ||
43 | + } | ||
44 | + | ||
45 | + ngOnInit(): void { | ||
46 | + } | ||
47 | + | ||
48 | + close(): void { | ||
49 | + this.dialogRef.close(); | ||
50 | + } | ||
51 | + | ||
52 | + onActivationLinkCopied() { | ||
53 | + this.store.dispatch(new ActionNotificationShow( | ||
54 | + { | ||
55 | + message: this.translate.instant('user.activation-link-copied-message'), | ||
56 | + type: 'success', | ||
57 | + target: 'activationLinkDialogContent', | ||
58 | + duration: 1200, | ||
59 | + verticalPosition: 'bottom', | ||
60 | + horizontalPosition: 'left' | ||
61 | + })); | ||
62 | + } | ||
63 | + | ||
64 | +} |
1 | +<!-- | ||
2 | + | ||
3 | + Copyright © 2016-2019 The Thingsboard Authors | ||
4 | + | ||
5 | + Licensed under the Apache License, Version 2.0 (the "License"); | ||
6 | + you may not use this file except in compliance with the License. | ||
7 | + You may obtain a copy of the License at | ||
8 | + | ||
9 | + http://www.apache.org/licenses/LICENSE-2.0 | ||
10 | + | ||
11 | + Unless required by applicable law or agreed to in writing, software | ||
12 | + distributed under the License is distributed on an "AS IS" BASIS, | ||
13 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
14 | + See the License for the specific language governing permissions and | ||
15 | + limitations under the License. | ||
16 | + | ||
17 | +--> | ||
18 | +<form (ngSubmit)="add()" style="width: 600px;"> | ||
19 | + <mat-toolbar fxLayout="row" color="primary"> | ||
20 | + <h2 translate>user.add</h2> | ||
21 | + <span fxFlex></span> | ||
22 | + <div [tb-help]="'user'"></div> | ||
23 | + <button mat-button mat-icon-button | ||
24 | + (click)="cancel()" | ||
25 | + type="button"> | ||
26 | + <mat-icon class="material-icons">close</mat-icon> | ||
27 | + </button> | ||
28 | + </mat-toolbar> | ||
29 | + <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> | ||
30 | + </mat-progress-bar> | ||
31 | + <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div> | ||
32 | + <div mat-dialog-content> | ||
33 | + <tb-user></tb-user> | ||
34 | + <mat-form-field class="mat-block"> | ||
35 | + <mat-label translate>user.activation-method</mat-label> | ||
36 | + <mat-select matInput [ngModelOptions]="{standalone: true}" [(ngModel)]="activationMethod"> | ||
37 | + <mat-option *ngFor="let activationMethod of (activationMethods | enumToArray)" [value]="activationMethods[activationMethod]"> | ||
38 | + {{ activationMethodTranslations.get(activationMethods[activationMethod]) | translate }} | ||
39 | + </mat-option> | ||
40 | + </mat-select> | ||
41 | + </mat-form-field> | ||
42 | + </div> | ||
43 | + <div mat-dialog-actions fxLayout="row"> | ||
44 | + <span fxFlex></span> | ||
45 | + <button mat-button mat-raised-button color="primary" | ||
46 | + type="submit" | ||
47 | + [disabled]="(isLoading$ | async) || detailsForm.invalid || !detailsForm.dirty"> | ||
48 | + {{ 'action.add' | translate }} | ||
49 | + </button> | ||
50 | + <button mat-button color="primary" | ||
51 | + style="margin-right: 20px;" | ||
52 | + type="button" | ||
53 | + cdkFocusInitial | ||
54 | + [disabled]="(isLoading$ | async)" | ||
55 | + (click)="cancel()"> | ||
56 | + {{ 'action.cancel' | translate }} | ||
57 | + </button> | ||
58 | + </div> | ||
59 | +</form> |
1 | +/// | ||
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | ||
3 | +/// | ||
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | +/// you may not use this file except in compliance with the License. | ||
6 | +/// You may obtain a copy of the License at | ||
7 | +/// | ||
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | +/// | ||
10 | +/// Unless required by applicable law or agreed to in writing, software | ||
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | +/// See the License for the specific language governing permissions and | ||
14 | +/// limitations under the License. | ||
15 | +/// | ||
16 | + | ||
17 | +import { Component, Inject, OnInit, ViewChild } from '@angular/core'; | ||
18 | +import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material'; | ||
19 | +import { PageComponent } from '@shared/components/page.component'; | ||
20 | +import { Store } from '@ngrx/store'; | ||
21 | +import { AppState } from '@core/core.state'; | ||
22 | +import { NgForm } from '@angular/forms'; | ||
23 | +import { UserComponent } from '@modules/home/pages/user/user.component'; | ||
24 | +import { Authority } from '@shared/models/authority.enum'; | ||
25 | +import { ActivationMethod, activationMethodTranslations, User } from '@shared/models/user.model'; | ||
26 | +import { CustomerId } from '@shared/models/id/customer-id'; | ||
27 | +import { UserService } from '@core/http/user.service'; | ||
28 | +import { Observable } from 'rxjs'; | ||
29 | +import { | ||
30 | + ActivationLinkDialogComponent, | ||
31 | + ActivationLinkDialogData | ||
32 | +} from '@modules/home/pages/user/activation-link-dialog.component'; | ||
33 | +import {TenantId} from '@app/shared/models/id/tenant-id'; | ||
34 | + | ||
35 | +export interface AddUserDialogData { | ||
36 | + tenantId: string; | ||
37 | + customerId: string; | ||
38 | + authority: Authority; | ||
39 | +} | ||
40 | + | ||
41 | +@Component({ | ||
42 | + selector: 'tb-add-user-dialog', | ||
43 | + templateUrl: './add-user-dialog.component.html' | ||
44 | +}) | ||
45 | +export class AddUserDialogComponent extends PageComponent implements OnInit { | ||
46 | + | ||
47 | + detailsForm: NgForm; | ||
48 | + user: User; | ||
49 | + | ||
50 | + activationMethods = ActivationMethod; | ||
51 | + | ||
52 | + activationMethodTranslations = activationMethodTranslations; | ||
53 | + | ||
54 | + activationMethod = ActivationMethod.DISPLAY_ACTIVATION_LINK; | ||
55 | + | ||
56 | + @ViewChild(UserComponent, {static: true}) userComponent: UserComponent; | ||
57 | + | ||
58 | + constructor(protected store: Store<AppState>, | ||
59 | + @Inject(MAT_DIALOG_DATA) public data: AddUserDialogData, | ||
60 | + public dialogRef: MatDialogRef<AddUserDialogComponent, User>, | ||
61 | + private userService: UserService, | ||
62 | + private dialog: MatDialog) { | ||
63 | + super(store); | ||
64 | + } | ||
65 | + | ||
66 | + ngOnInit(): void { | ||
67 | + this.user = {} as User; | ||
68 | + this.userComponent.isEdit = true; | ||
69 | + this.userComponent.entity = this.user; | ||
70 | + this.detailsForm = this.userComponent.entityNgForm; | ||
71 | + } | ||
72 | + | ||
73 | + cancel(): void { | ||
74 | + this.dialogRef.close(null); | ||
75 | + } | ||
76 | + | ||
77 | + add(): void { | ||
78 | + if (this.detailsForm.valid) { | ||
79 | + this.user = {...this.user, ...this.userComponent.entityForm.value}; | ||
80 | + this.user.authority = this.data.authority; | ||
81 | + this.user.tenantId = new TenantId(this.data.tenantId); | ||
82 | + this.user.customerId = new CustomerId(this.data.customerId); | ||
83 | + const sendActivationEmail = this.activationMethod === ActivationMethod.SEND_ACTIVATION_MAIL; | ||
84 | + this.userService.saveUser(this.user, sendActivationEmail).subscribe( | ||
85 | + (user) => { | ||
86 | + if (this.activationMethod === ActivationMethod.DISPLAY_ACTIVATION_LINK) { | ||
87 | + this.userService.getActivationLink(user.id.id).subscribe( | ||
88 | + (activationLink) => { | ||
89 | + this.displayActivationLink(activationLink).subscribe( | ||
90 | + () => { | ||
91 | + this.dialogRef.close(user); | ||
92 | + } | ||
93 | + ); | ||
94 | + } | ||
95 | + ); | ||
96 | + } else { | ||
97 | + this.dialogRef.close(user); | ||
98 | + } | ||
99 | + } | ||
100 | + ); | ||
101 | + } | ||
102 | + } | ||
103 | + | ||
104 | + displayActivationLink(activationLink: string): Observable<void> { | ||
105 | + return this.dialog.open<ActivationLinkDialogComponent, ActivationLinkDialogData, | ||
106 | + void>(ActivationLinkDialogComponent, { | ||
107 | + disableClose: true, | ||
108 | + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], | ||
109 | + data: { | ||
110 | + activationLink | ||
111 | + } | ||
112 | + }).afterClosed(); | ||
113 | + } | ||
114 | +} |
1 | +/// | ||
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | ||
3 | +/// | ||
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | +/// you may not use this file except in compliance with the License. | ||
6 | +/// You may obtain a copy of the License at | ||
7 | +/// | ||
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | +/// | ||
10 | +/// Unless required by applicable law or agreed to in writing, software | ||
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | +/// See the License for the specific language governing permissions and | ||
14 | +/// limitations under the License. | ||
15 | +/// | ||
16 | + | ||
17 | +import { NgModule } from '@angular/core'; | ||
18 | +import { RouterModule, Routes } from '@angular/router'; | ||
19 | +import { UsersTableConfigResolver } from '@modules/home/pages/user/users-table-config.resolver'; | ||
20 | + | ||
21 | +@NgModule({ | ||
22 | + imports: [], | ||
23 | + exports: [RouterModule], | ||
24 | + providers: [ | ||
25 | + UsersTableConfigResolver | ||
26 | + ] | ||
27 | +}) | ||
28 | +export class UserRoutingModule { } |
1 | +<!-- | ||
2 | + | ||
3 | + Copyright © 2016-2019 The Thingsboard Authors | ||
4 | + | ||
5 | + Licensed under the Apache License, Version 2.0 (the "License"); | ||
6 | + you may not use this file except in compliance with the License. | ||
7 | + You may obtain a copy of the License at | ||
8 | + | ||
9 | + http://www.apache.org/licenses/LICENSE-2.0 | ||
10 | + | ||
11 | + Unless required by applicable law or agreed to in writing, software | ||
12 | + distributed under the License is distributed on an "AS IS" BASIS, | ||
13 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
14 | + See the License for the specific language governing permissions and | ||
15 | + limitations under the License. | ||
16 | + | ||
17 | +--> | ||
18 | +<div class="tb-details-buttons"> | ||
19 | + <button mat-raised-button color="primary" | ||
20 | + [disabled]="(isLoading$ | async)" | ||
21 | + (click)="onEntityAction($event, 'displayActivationLink')" | ||
22 | + [fxShow]="!isEdit"> | ||
23 | + {{'user.display-activation-link' | translate }} | ||
24 | + </button> | ||
25 | + <button mat-raised-button color="primary" | ||
26 | + [disabled]="(isLoading$ | async)" | ||
27 | + (click)="onEntityAction($event, 'resendActivation')" | ||
28 | + [fxShow]="!isEdit"> | ||
29 | + {{'user.resend-activation' | translate }} | ||
30 | + </button> | ||
31 | + <button mat-raised-button color="primary" | ||
32 | + [disabled]="(isLoading$ | async)" | ||
33 | + (click)="onEntityAction($event, 'loginAsUser')" | ||
34 | + *ngIf="loginAsUserEnabled$ | async" | ||
35 | + [fxShow]="!isEdit"> | ||
36 | + {{ (entity?.authority === authority.TENANT_ADMIN ? 'user.login-as-tenant-admin' : 'user.login-as-customer-user') | translate }} | ||
37 | + </button> | ||
38 | + <button mat-raised-button color="primary" | ||
39 | + [disabled]="(isLoading$ | async)" | ||
40 | + (click)="onEntityAction($event, 'delete')" | ||
41 | + [fxShow]="!hideDelete() && !isEdit"> | ||
42 | + {{'user.delete' | translate }} | ||
43 | + </button> | ||
44 | +</div> | ||
45 | +<div class="mat-padding" fxLayout="column"> | ||
46 | + <form #entityNgForm="ngForm" [formGroup]="entityForm"> | ||
47 | + <fieldset [disabled]="(isLoading$ | async) || !isEdit"> | ||
48 | + <mat-form-field class="mat-block"> | ||
49 | + <mat-label translate>user.email</mat-label> | ||
50 | + <input matInput formControlName="email" required> | ||
51 | + <mat-error *ngIf="entityForm.get('email').hasError('email')"> | ||
52 | + {{ 'user.invalid-email-format' | translate }} | ||
53 | + </mat-error> | ||
54 | + <mat-error *ngIf="entityForm.get('email').hasError('required')"> | ||
55 | + {{ 'user.email-required' | translate }} | ||
56 | + </mat-error> | ||
57 | + </mat-form-field> | ||
58 | + <mat-form-field class="mat-block"> | ||
59 | + <mat-label translate>user.first-name</mat-label> | ||
60 | + <input matInput formControlName="firstName"> | ||
61 | + </mat-form-field> | ||
62 | + <mat-form-field class="mat-block"> | ||
63 | + <mat-label translate>user.last-name</mat-label> | ||
64 | + <input matInput formControlName="lastName"> | ||
65 | + </mat-form-field> | ||
66 | + <div formGroupName="additionalInfo" fxLayout="column"> | ||
67 | + <mat-form-field class="mat-block"> | ||
68 | + <mat-label translate>user.description</mat-label> | ||
69 | + <textarea matInput formControlName="description" rows="2"></textarea> | ||
70 | + </mat-form-field> | ||
71 | + <section class="tb-default-dashboard" fxFlex fxLayout="column" *ngIf="entity?.id"> | ||
72 | + <section fxFlex fxLayout="column" fxLayout.gt-sm="row"> | ||
73 | + <tb-dashboard-autocomplete | ||
74 | + fxFlex | ||
75 | + placeholder="{{ 'user.default-dashboard' | translate }}" | ||
76 | + formControlName="defaultDashboardId" | ||
77 | + [dashboardsScope]="entity?.authority === authority.TENANT_ADMIN ? 'tenant' : 'customer'" | ||
78 | + [tenantId]="entity?.tenantId?.id" | ||
79 | + [customerId]="entity?.customerId?.id" | ||
80 | + [selectFirstDashboard]="false" | ||
81 | + ></tb-dashboard-autocomplete> | ||
82 | + <mat-checkbox fxFlex formControlName="defaultDashboardFullscreen"> | ||
83 | + {{ 'user.always-fullscreen' | translate }} | ||
84 | + </mat-checkbox> | ||
85 | + </section> | ||
86 | + </section> | ||
87 | + </div> | ||
88 | + </fieldset> | ||
89 | + </form> | ||
90 | +</div> |
1 | +/** | ||
2 | + * Copyright © 2016-2019 The Thingsboard Authors | ||
3 | + * | ||
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | + * you may not use this file except in compliance with the License. | ||
6 | + * You may obtain a copy of the License at | ||
7 | + * | ||
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | + * | ||
10 | + * Unless required by applicable law or agreed to in writing, software | ||
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | + * See the License for the specific language governing permissions and | ||
14 | + * limitations under the License. | ||
15 | + */ | ||
16 | +@import "../../../../../scss/constants"; | ||
17 | + | ||
18 | +:host { | ||
19 | + .tb-default-dashboard { | ||
20 | + tb-dashboard-autocomplete { | ||
21 | + @media #{$mat-gt-sm} { | ||
22 | + padding-right: 12px; | ||
23 | + } | ||
24 | + | ||
25 | + @media #{$mat-lt-md} { | ||
26 | + padding-bottom: 12px; | ||
27 | + } | ||
28 | + } | ||
29 | + mat-checkbox { | ||
30 | + @media #{$mat-gt-sm} { | ||
31 | + margin-top: 16px; | ||
32 | + } | ||
33 | + } | ||
34 | + } | ||
35 | +} |
1 | +/// | ||
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | ||
3 | +/// | ||
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | +/// you may not use this file except in compliance with the License. | ||
6 | +/// You may obtain a copy of the License at | ||
7 | +/// | ||
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | +/// | ||
10 | +/// Unless required by applicable law or agreed to in writing, software | ||
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | +/// See the License for the specific language governing permissions and | ||
14 | +/// limitations under the License. | ||
15 | +/// | ||
16 | + | ||
17 | +import { Component, OnInit } from '@angular/core'; | ||
18 | +import { select, Store } from '@ngrx/store'; | ||
19 | +import { AppState } from '@core/core.state'; | ||
20 | +import { EntityComponent } from '@shared/components/entity/entity.component'; | ||
21 | +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; | ||
22 | +import { User } from '@shared/models/user.model'; | ||
23 | +import { selectAuth, selectUserDetails } from '@core/auth/auth.selectors'; | ||
24 | +import { map } from 'rxjs/operators'; | ||
25 | +import { Authority } from '@shared/models/authority.enum'; | ||
26 | + | ||
27 | +@Component({ | ||
28 | + selector: 'tb-user', | ||
29 | + templateUrl: './user.component.html', | ||
30 | + styleUrls: ['./user.component.scss'] | ||
31 | +}) | ||
32 | +export class UserComponent extends EntityComponent<User> { | ||
33 | + | ||
34 | + authority = Authority; | ||
35 | + | ||
36 | + loginAsUserEnabled$ = this.store.pipe( | ||
37 | + select(selectAuth), | ||
38 | + map((auth) => auth.userTokenAccessEnabled) | ||
39 | + ); | ||
40 | + | ||
41 | + constructor(protected store: Store<AppState>, | ||
42 | + public fb: FormBuilder) { | ||
43 | + super(store); | ||
44 | + } | ||
45 | + | ||
46 | + hideDelete() { | ||
47 | + if (this.entitiesTableConfig) { | ||
48 | + return !this.entitiesTableConfig.deleteEnabled(this.entity); | ||
49 | + } else { | ||
50 | + return false; | ||
51 | + } | ||
52 | + } | ||
53 | + | ||
54 | + buildForm(entity: User): FormGroup { | ||
55 | + return this.fb.group( | ||
56 | + { | ||
57 | + email: [entity ? entity.email : '', [Validators.required, Validators.email]], | ||
58 | + firstName: [entity ? entity.firstName : ''], | ||
59 | + lastName: [entity ? entity.lastName : ''], | ||
60 | + additionalInfo: this.fb.group( | ||
61 | + { | ||
62 | + description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''], | ||
63 | + defaultDashboardId: [entity && entity.additionalInfo ? entity.additionalInfo.defaultDashboardId : null], | ||
64 | + defaultDashboardFullscreen: [entity && entity.additionalInfo ? entity.additionalInfo.defaultDashboardFullscreen : false], | ||
65 | + } | ||
66 | + ) | ||
67 | + } | ||
68 | + ); | ||
69 | + } | ||
70 | + | ||
71 | + updateForm(entity: User) { | ||
72 | + this.entityForm.patchValue({email: entity.email}); | ||
73 | + this.entityForm.patchValue({firstName: entity.firstName}); | ||
74 | + this.entityForm.patchValue({lastName: entity.lastName}); | ||
75 | + this.entityForm.patchValue({additionalInfo: {description: entity.additionalInfo ? entity.additionalInfo.description : ''}}); | ||
76 | + this.entityForm.patchValue({additionalInfo: | ||
77 | + {defaultDashboardId: entity.additionalInfo ? entity.additionalInfo.defaultDashboardId : null}}); | ||
78 | + this.entityForm.patchValue({additionalInfo: | ||
79 | + {defaultDashboardFullscreen: entity.additionalInfo ? entity.additionalInfo.defaultDashboardFullscreen : false}}); | ||
80 | + } | ||
81 | + | ||
82 | +} |
1 | +/// | ||
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | ||
3 | +/// | ||
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | +/// you may not use this file except in compliance with the License. | ||
6 | +/// You may obtain a copy of the License at | ||
7 | +/// | ||
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | +/// | ||
10 | +/// Unless required by applicable law or agreed to in writing, software | ||
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | +/// See the License for the specific language governing permissions and | ||
14 | +/// limitations under the License. | ||
15 | +/// | ||
16 | + | ||
17 | +import { NgModule } from '@angular/core'; | ||
18 | +import { CommonModule } from '@angular/common'; | ||
19 | +import { SharedModule } from '@shared/shared.module'; | ||
20 | +import { UserComponent } from '@modules/home/pages/user/user.component'; | ||
21 | +import { UserRoutingModule } from '@modules/home/pages/user/user-routing.module'; | ||
22 | +import { AddUserDialogComponent } from '@modules/home/pages/user/add-user-dialog.component'; | ||
23 | +import { ActivationLinkDialogComponent } from '@modules/home/pages/user/activation-link-dialog.component'; | ||
24 | + | ||
25 | +@NgModule({ | ||
26 | + entryComponents: [ | ||
27 | + UserComponent, | ||
28 | + AddUserDialogComponent, | ||
29 | + ActivationLinkDialogComponent | ||
30 | + ], | ||
31 | + declarations: [ | ||
32 | + UserComponent, | ||
33 | + AddUserDialogComponent, | ||
34 | + ActivationLinkDialogComponent | ||
35 | + ], | ||
36 | + imports: [ | ||
37 | + CommonModule, | ||
38 | + SharedModule, | ||
39 | + UserRoutingModule | ||
40 | + ] | ||
41 | +}) | ||
42 | +export class UserModule { } |
1 | +/// | ||
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | ||
3 | +/// | ||
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | +/// you may not use this file except in compliance with the License. | ||
6 | +/// You may obtain a copy of the License at | ||
7 | +/// | ||
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | +/// | ||
10 | +/// Unless required by applicable law or agreed to in writing, software | ||
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | +/// See the License for the specific language governing permissions and | ||
14 | +/// limitations under the License. | ||
15 | +/// | ||
16 | + | ||
17 | +import { Injectable } from '@angular/core'; | ||
18 | + | ||
19 | +import { ActivatedRouteSnapshot, Resolve } from '@angular/router'; | ||
20 | +import { | ||
21 | + DateEntityTableColumn, | ||
22 | + EntityTableColumn, | ||
23 | + EntityTableConfig | ||
24 | +} from '@shared/components/entity/entities-table-config.models'; | ||
25 | +import { TranslateService } from '@ngx-translate/core'; | ||
26 | +import { DatePipe } from '@angular/common'; | ||
27 | +import { | ||
28 | + EntityType, | ||
29 | + entityTypeResources, | ||
30 | + entityTypeTranslations | ||
31 | +} from '@shared/models/entity-type.models'; | ||
32 | +import { User } from '@shared/models/user.model'; | ||
33 | +import { UserService } from '@core/http/user.service'; | ||
34 | +import { UserComponent } from '@modules/home/pages/user/user.component'; | ||
35 | +import { CustomerService } from '@core/http/customer.service'; | ||
36 | +import { map, mergeMap, take, tap } from 'rxjs/operators'; | ||
37 | +import { forkJoin, noop, Observable, of } from 'rxjs'; | ||
38 | +import { Authority } from '@shared/models/authority.enum'; | ||
39 | +import { CustomerId } from '@shared/models/id/customer-id'; | ||
40 | +import { MatDialog } from '@angular/material'; | ||
41 | +import { EntityAction } from '@shared/components/entity/entity-component.models'; | ||
42 | +import { | ||
43 | + AddUserDialogComponent, | ||
44 | + AddUserDialogData | ||
45 | +} from '@modules/home/pages/user/add-user-dialog.component'; | ||
46 | +import { AuthState } from '@core/auth/auth.models'; | ||
47 | +import { select, Store } from '@ngrx/store'; | ||
48 | +import { AppState } from '@core/core.state'; | ||
49 | +import { selectAuth } from '@core/auth/auth.selectors'; | ||
50 | +import { AuthService } from '@core/auth/auth.service'; | ||
51 | +import { | ||
52 | + ActivationLinkDialogComponent, | ||
53 | + ActivationLinkDialogData | ||
54 | +} from '@modules/home/pages/user/activation-link-dialog.component'; | ||
55 | +import { ActionNotificationShow } from '@core/notification/notification.actions'; | ||
56 | +import { NULL_UUID } from '@shared/models/id/has-uuid'; | ||
57 | +import { Customer } from '@shared/models/customer.model'; | ||
58 | +import {TenantService} from '@app/core/http/tenant.service'; | ||
59 | +import {TenantId} from '@app/shared/models/id/tenant-id'; | ||
60 | + | ||
61 | +export interface UsersTableRouteData { | ||
62 | + authority: Authority; | ||
63 | +} | ||
64 | + | ||
65 | +@Injectable() | ||
66 | +export class UsersTableConfigResolver implements Resolve<EntityTableConfig<User>> { | ||
67 | + | ||
68 | + private readonly config: EntityTableConfig<User> = new EntityTableConfig<User>(); | ||
69 | + | ||
70 | + private tenantId: string; | ||
71 | + private customerId: string; | ||
72 | + private authority: Authority; | ||
73 | + private authUser: User; | ||
74 | + | ||
75 | + constructor(private store: Store<AppState>, | ||
76 | + private userService: UserService, | ||
77 | + private authService: AuthService, | ||
78 | + private tenantService: TenantService, | ||
79 | + private customerService: CustomerService, | ||
80 | + private translate: TranslateService, | ||
81 | + private datePipe: DatePipe, | ||
82 | + private dialog: MatDialog) { | ||
83 | + | ||
84 | + this.config.entityType = EntityType.USER; | ||
85 | + this.config.entityComponent = UserComponent; | ||
86 | + this.config.entityTranslations = entityTypeTranslations.get(EntityType.USER); | ||
87 | + this.config.entityResources = entityTypeResources.get(EntityType.USER); | ||
88 | + | ||
89 | + this.config.columns.push( | ||
90 | + new DateEntityTableColumn<User>('createdTime', 'user.created-time', this.datePipe, '150px'), | ||
91 | + new EntityTableColumn<User>('firstName', 'user.first-name'), | ||
92 | + new EntityTableColumn<User>('lastName', 'user.last-name'), | ||
93 | + new EntityTableColumn<User>('email', 'user.email') | ||
94 | + ); | ||
95 | + | ||
96 | + this.config.deleteEnabled = user => user && user.id && user.id.id !== this.authUser.id.id; | ||
97 | + this.config.deleteEntityTitle = user => this.translate.instant('user.delete-user-title', { userEmail: user.email }); | ||
98 | + this.config.deleteEntityContent = () => this.translate.instant('user.delete-user-text'); | ||
99 | + this.config.deleteEntitiesTitle = count => this.translate.instant('user.delete-users-title', {count}); | ||
100 | + this.config.deleteEntitiesContent = () => this.translate.instant('user.delete-users-text'); | ||
101 | + | ||
102 | + this.config.loadEntity = id => this.userService.getUser(id.id); | ||
103 | + this.config.saveEntity = user => this.saveUser(user); | ||
104 | + this.config.deleteEntity = id => this.userService.deleteUser(id.id); | ||
105 | + this.config.onEntityAction = action => this.onUserAction(action); | ||
106 | + this.config.addEntity = () => this.addUser(); | ||
107 | + } | ||
108 | + | ||
109 | + resolve(route: ActivatedRouteSnapshot): Observable<EntityTableConfig<User>> { | ||
110 | + const routeParams = route.params; | ||
111 | + return this.store.pipe(select(selectAuth), take(1)).pipe( | ||
112 | + tap((auth) => { | ||
113 | + this.authUser = auth.userDetails; | ||
114 | + this.authority = routeParams.tenantId ? Authority.TENANT_ADMIN : Authority.CUSTOMER_USER; | ||
115 | + if (this.authority === Authority.TENANT_ADMIN) { | ||
116 | + this.tenantId = routeParams.tenantId; | ||
117 | + this.customerId = NULL_UUID; | ||
118 | + this.config.entitiesFetchFunction = pageLink => this.userService.getTenantAdmins(this.tenantId, pageLink); | ||
119 | + } else { | ||
120 | + this.tenantId = this.authUser.tenantId.id; | ||
121 | + this.customerId = routeParams.customerId; | ||
122 | + this.config.entitiesFetchFunction = pageLink => this.userService.getCustomerUsers(this.customerId, pageLink); | ||
123 | + } | ||
124 | + this.updateActionCellDescriptors(auth); | ||
125 | + }), | ||
126 | + mergeMap(() => this.authority === Authority.TENANT_ADMIN ? | ||
127 | + this.tenantService.getTenant(this.tenantId) : | ||
128 | + this.customerService.getCustomer(this.customerId)), | ||
129 | + map((parentEntity) => { | ||
130 | + if (this.authority === Authority.TENANT_ADMIN) { | ||
131 | + this.config.tableTitle = parentEntity.title + ': ' + this.translate.instant('user.tenant-admins'); | ||
132 | + } else { | ||
133 | + this.config.tableTitle = parentEntity.title + ': ' + this.translate.instant('user.customer-users'); | ||
134 | + } | ||
135 | + return this.config; | ||
136 | + }) | ||
137 | + ); | ||
138 | + } | ||
139 | + | ||
140 | + updateActionCellDescriptors(auth: AuthState) { | ||
141 | + this.config.cellActionDescriptors.splice(0); | ||
142 | + if (auth.userTokenAccessEnabled) { | ||
143 | + this.config.cellActionDescriptors.push( | ||
144 | + { | ||
145 | + name: this.authority === Authority.TENANT_ADMIN ? | ||
146 | + this.translate.instant('user.login-as-tenant-admin') : | ||
147 | + this.translate.instant('user.login-as-customer-user'), | ||
148 | + icon: 'mdi:login', | ||
149 | + isMdiIcon: true, | ||
150 | + isEnabled: () => true, | ||
151 | + onAction: ($event, entity) => this.loginAsUser($event, entity) | ||
152 | + } | ||
153 | + ); | ||
154 | + } | ||
155 | + } | ||
156 | + | ||
157 | + saveUser(user: User): Observable<User> { | ||
158 | + user.tenantId = new TenantId(this.tenantId); | ||
159 | + user.customerId = new CustomerId(this.customerId); | ||
160 | + user.authority = this.authority; | ||
161 | + return this.userService.saveUser(user); | ||
162 | + } | ||
163 | + | ||
164 | + addUser(): Observable<User> { | ||
165 | + return this.dialog.open<AddUserDialogComponent, AddUserDialogData, | ||
166 | + User>(AddUserDialogComponent, { | ||
167 | + disableClose: true, | ||
168 | + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], | ||
169 | + data: { | ||
170 | + tenantId: this.tenantId, | ||
171 | + customerId: this.customerId, | ||
172 | + authority: this.authority | ||
173 | + } | ||
174 | + }).afterClosed(); | ||
175 | + } | ||
176 | + | ||
177 | + loginAsUser($event: Event, user: User) { | ||
178 | + if ($event) { | ||
179 | + $event.stopPropagation(); | ||
180 | + } | ||
181 | + this.authService.loginAsUser(user.id.id).subscribe(); | ||
182 | + } | ||
183 | + | ||
184 | + displayActivationLink($event: Event, user: User) { | ||
185 | + if ($event) { | ||
186 | + $event.stopPropagation(); | ||
187 | + } | ||
188 | + this.userService.getActivationLink(user.id.id).subscribe( | ||
189 | + (activationLink) => { | ||
190 | + this.dialog.open<ActivationLinkDialogComponent, ActivationLinkDialogData, | ||
191 | + void>(ActivationLinkDialogComponent, { | ||
192 | + disableClose: true, | ||
193 | + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], | ||
194 | + data: { | ||
195 | + activationLink | ||
196 | + } | ||
197 | + }); | ||
198 | + } | ||
199 | + ); | ||
200 | + } | ||
201 | + | ||
202 | + resendActivation($event: Event, user: User) { | ||
203 | + if ($event) { | ||
204 | + $event.stopPropagation(); | ||
205 | + } | ||
206 | + this.userService.sendActivationEmail(user.email).subscribe(() => { | ||
207 | + this.store.dispatch(new ActionNotificationShow( | ||
208 | + { | ||
209 | + message: this.translate.instant('user.activation-email-sent-message'), | ||
210 | + type: 'success' | ||
211 | + })); | ||
212 | + }); | ||
213 | + } | ||
214 | + | ||
215 | + onUserAction(action: EntityAction<User>): boolean { | ||
216 | + switch (action.action) { | ||
217 | + case 'loginAsUser': | ||
218 | + this.loginAsUser(action.event, action.entity); | ||
219 | + return true; | ||
220 | + case 'displayActivationLink': | ||
221 | + this.displayActivationLink(action.event, action.entity); | ||
222 | + return true; | ||
223 | + case 'resendActivation': | ||
224 | + this.resendActivation(action.event, action.entity); | ||
225 | + return true; | ||
226 | + } | ||
227 | + return false; | ||
228 | + } | ||
229 | + | ||
230 | +} |
1 | +<!-- | ||
2 | + | ||
3 | + Copyright © 2016-2019 The Thingsboard Authors | ||
4 | + | ||
5 | + Licensed under the Apache License, Version 2.0 (the "License"); | ||
6 | + you may not use this file except in compliance with the License. | ||
7 | + You may obtain a copy of the License at | ||
8 | + | ||
9 | + http://www.apache.org/licenses/LICENSE-2.0 | ||
10 | + | ||
11 | + Unless required by applicable law or agreed to in writing, software | ||
12 | + distributed under the License is distributed on an "AS IS" BASIS, | ||
13 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
14 | + See the License for the specific language governing permissions and | ||
15 | + limitations under the License. | ||
16 | + | ||
17 | +--> | ||
18 | +<section [formGroup]="parentForm"> | ||
19 | + <mat-form-field class="mat-block"> | ||
20 | + <mat-label translate>contact.country</mat-label> | ||
21 | + <mat-select matInput formControlName="country"> | ||
22 | + <mat-option *ngFor="let country of countries" [value]="country"> | ||
23 | + {{ country }} | ||
24 | + </mat-option> | ||
25 | + </mat-select> | ||
26 | + </mat-form-field> | ||
27 | + <div fxLayout.gt-sm="row" fxLayoutGap.gt-sm="10px"> | ||
28 | + <mat-form-field class="mat-block"> | ||
29 | + <mat-label translate>contact.city</mat-label> | ||
30 | + <input matInput formControlName="city"> | ||
31 | + </mat-form-field> | ||
32 | + <mat-form-field class="mat-block"> | ||
33 | + <mat-label translate>contact.state</mat-label> | ||
34 | + <input matInput formControlName="state"> | ||
35 | + </mat-form-field> | ||
36 | + <mat-form-field class="mat-block"> | ||
37 | + <mat-label translate>contact.postal-code</mat-label> | ||
38 | + <input matInput formControlName="zip"> | ||
39 | + <mat-error *ngIf="parentForm.get('zip').hasError('pattern')"> | ||
40 | + {{ 'contact.postal-code-invalid' | translate }} | ||
41 | + </mat-error> | ||
42 | + </mat-form-field> | ||
43 | + </div> | ||
44 | + <mat-form-field class="mat-block"> | ||
45 | + <mat-label translate>contact.address</mat-label> | ||
46 | + <input matInput formControlName="address"> | ||
47 | + </mat-form-field> | ||
48 | + <mat-form-field class="mat-block"> | ||
49 | + <mat-label translate>contact.address2</mat-label> | ||
50 | + <input matInput formControlName="address2"> | ||
51 | + </mat-form-field> | ||
52 | + <mat-form-field class="mat-block"> | ||
53 | + <mat-label translate>contact.phone</mat-label> | ||
54 | + <input matInput formControlName="phone"> | ||
55 | + </mat-form-field> | ||
56 | + <mat-form-field class="mat-block"> | ||
57 | + <mat-label translate>contact.email</mat-label> | ||
58 | + <input matInput formControlName="email"> | ||
59 | + <mat-error *ngIf="parentForm.get('email').hasError('email')"> | ||
60 | + {{ 'user.invalid-email-format' | translate }} | ||
61 | + </mat-error> | ||
62 | + </mat-form-field> | ||
63 | +</section> |
1 | +/// | ||
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | ||
3 | +/// | ||
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | +/// you may not use this file except in compliance with the License. | ||
6 | +/// You may obtain a copy of the License at | ||
7 | +/// | ||
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | +/// | ||
10 | +/// Unless required by applicable law or agreed to in writing, software | ||
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | +/// See the License for the specific language governing permissions and | ||
14 | +/// limitations under the License. | ||
15 | +/// | ||
16 | + | ||
17 | +import { Component, Input } from '@angular/core'; | ||
18 | +import { FormGroup } from '@angular/forms'; | ||
19 | +import { COUNTRIES } from '@shared/components/contact.models'; | ||
20 | + | ||
21 | +@Component({ | ||
22 | + selector: 'tb-contact', | ||
23 | + templateUrl: './contact.component.html' | ||
24 | +}) | ||
25 | +export class ContactComponent { | ||
26 | + | ||
27 | + @Input() | ||
28 | + parentForm: FormGroup; | ||
29 | + | ||
30 | + @Input() isEdit: boolean; | ||
31 | + | ||
32 | + countries = COUNTRIES; | ||
33 | + | ||
34 | +} |
1 | +/// | ||
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | ||
3 | +/// | ||
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | +/// you may not use this file except in compliance with the License. | ||
6 | +/// You may obtain a copy of the License at | ||
7 | +/// | ||
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | +/// | ||
10 | +/// Unless required by applicable law or agreed to in writing, software | ||
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | +/// See the License for the specific language governing permissions and | ||
14 | +/// limitations under the License. | ||
15 | +/// | ||
16 | + | ||
17 | +export const COUNTRIES = [ | ||
18 | + 'Afghanistan', | ||
19 | + 'Åland Islands', | ||
20 | + 'Albania', | ||
21 | + 'Algeria', | ||
22 | + 'American Samoa', | ||
23 | + 'Andorra', | ||
24 | + 'Angola', | ||
25 | + 'Anguilla', | ||
26 | + 'Antarctica', | ||
27 | + 'Antigua and Barbuda', | ||
28 | + 'Argentina', | ||
29 | + 'Armenia', | ||
30 | + 'Aruba', | ||
31 | + 'Australia', | ||
32 | + 'Austria', | ||
33 | + 'Azerbaijan', | ||
34 | + 'Bahamas', | ||
35 | + 'Bahrain', | ||
36 | + 'Bangladesh', | ||
37 | + 'Barbados', | ||
38 | + 'Belarus', | ||
39 | + 'Belgium', | ||
40 | + 'Belize', | ||
41 | + 'Benin', | ||
42 | + 'Bermuda', | ||
43 | + 'Bhutan', | ||
44 | + 'Bolivia', | ||
45 | + 'Bonaire, Sint Eustatius and Saba', | ||
46 | + 'Bosnia and Herzegovina', | ||
47 | + 'Botswana', | ||
48 | + 'Bouvet Island', | ||
49 | + 'Brazil', | ||
50 | + 'British Indian Ocean Territory', | ||
51 | + 'Brunei Darussalam', | ||
52 | + 'Bulgaria', | ||
53 | + 'Burkina Faso', | ||
54 | + 'Burundi', | ||
55 | + 'Cambodia', | ||
56 | + 'Cameroon', | ||
57 | + 'Canada', | ||
58 | + 'Cape Verde', | ||
59 | + 'Cayman Islands', | ||
60 | + 'Central African Republic', | ||
61 | + 'Chad', | ||
62 | + 'Chile', | ||
63 | + 'China', | ||
64 | + 'Christmas Island', | ||
65 | + 'Cocos (Keeling) Islands', | ||
66 | + 'Colombia', | ||
67 | + 'Comoros', | ||
68 | + 'Congo', | ||
69 | + 'Congo, The Democratic Republic of the', | ||
70 | + 'Cook Islands', | ||
71 | + 'Costa Rica', | ||
72 | + 'Côte d\'Ivoire', | ||
73 | + 'Croatia', | ||
74 | + 'Cuba', | ||
75 | + 'Curaçao', | ||
76 | + 'Cyprus', | ||
77 | + 'Czech Republic', | ||
78 | + 'Denmark', | ||
79 | + 'Djibouti', | ||
80 | + 'Dominica', | ||
81 | + 'Dominican Republic', | ||
82 | + 'Ecuador', | ||
83 | + 'Egypt', | ||
84 | + 'El Salvador', | ||
85 | + 'Equatorial Guinea', | ||
86 | + 'Eritrea', | ||
87 | + 'Estonia', | ||
88 | + 'Ethiopia', | ||
89 | + 'Falkland Islands (Malvinas)', | ||
90 | + 'Faroe Islands', | ||
91 | + 'Fiji', | ||
92 | + 'Finland', | ||
93 | + 'France', | ||
94 | + 'French Guiana', | ||
95 | + 'French Polynesia', | ||
96 | + 'French Southern Territories', | ||
97 | + 'Gabon', | ||
98 | + 'Gambia', | ||
99 | + 'Georgia', | ||
100 | + 'Germany', | ||
101 | + 'Ghana', | ||
102 | + 'Gibraltar', | ||
103 | + 'Greece', | ||
104 | + 'Greenland', | ||
105 | + 'Grenada', | ||
106 | + 'Guadeloupe', | ||
107 | + 'Guam', | ||
108 | + 'Guatemala', | ||
109 | + 'Guernsey', | ||
110 | + 'Guinea', | ||
111 | + 'Guinea-Bissau', | ||
112 | + 'Guyana', | ||
113 | + 'Haiti', | ||
114 | + 'Heard Island and McDonald Islands', | ||
115 | + 'Holy See (Vatican City State)', | ||
116 | + 'Honduras', | ||
117 | + 'Hong Kong', | ||
118 | + 'Hungary', | ||
119 | + 'Iceland', | ||
120 | + 'India', | ||
121 | + 'Indonesia', | ||
122 | + 'Iran, Islamic Republic of', | ||
123 | + 'Iraq', | ||
124 | + 'Ireland', | ||
125 | + 'Isle of Man', | ||
126 | + 'Israel', | ||
127 | + 'Italy', | ||
128 | + 'Jamaica', | ||
129 | + 'Japan', | ||
130 | + 'Jersey', | ||
131 | + 'Jordan', | ||
132 | + 'Kazakhstan', | ||
133 | + 'Kenya', | ||
134 | + 'Kiribati', | ||
135 | + 'Korea, Democratic People\'s Republic of', | ||
136 | + 'Korea, Republic of', | ||
137 | + 'Kuwait', | ||
138 | + 'Kyrgyzstan', | ||
139 | + 'Lao People\'s Democratic Republic', | ||
140 | + 'Latvia', | ||
141 | + 'Lebanon', | ||
142 | + 'Lesotho', | ||
143 | + 'Liberia', | ||
144 | + 'Libya', | ||
145 | + 'Liechtenstein', | ||
146 | + 'Lithuania', | ||
147 | + 'Luxembourg', | ||
148 | + 'Macao', | ||
149 | + 'Macedonia, Republic Of', | ||
150 | + 'Madagascar', | ||
151 | + 'Malawi', | ||
152 | + 'Malaysia', | ||
153 | + 'Maldives', | ||
154 | + 'Mali', | ||
155 | + 'Malta', | ||
156 | + 'Marshall Islands', | ||
157 | + 'Martinique', | ||
158 | + 'Mauritania', | ||
159 | + 'Mauritius', | ||
160 | + 'Mayotte', | ||
161 | + 'Mexico', | ||
162 | + 'Micronesia, Federated States of', | ||
163 | + 'Moldova, Republic of', | ||
164 | + 'Monaco', | ||
165 | + 'Mongolia', | ||
166 | + 'Montenegro', | ||
167 | + 'Montserrat', | ||
168 | + 'Morocco', | ||
169 | + 'Mozambique', | ||
170 | + 'Myanmar', | ||
171 | + 'Namibia', | ||
172 | + 'Nauru', | ||
173 | + 'Nepal', | ||
174 | + 'Netherlands', | ||
175 | + 'New Caledonia', | ||
176 | + 'New Zealand', | ||
177 | + 'Nicaragua', | ||
178 | + 'Niger', | ||
179 | + 'Nigeria', | ||
180 | + 'Niue', | ||
181 | + 'Norfolk Island', | ||
182 | + 'Northern Mariana Islands', | ||
183 | + 'Norway', | ||
184 | + 'Oman', | ||
185 | + 'Pakistan', | ||
186 | + 'Palau', | ||
187 | + 'Palestinian Territory, Occupied', | ||
188 | + 'Panama', | ||
189 | + 'Papua New Guinea', | ||
190 | + 'Paraguay', | ||
191 | + 'Peru', | ||
192 | + 'Philippines', | ||
193 | + 'Pitcairn', | ||
194 | + 'Poland', | ||
195 | + 'Portugal', | ||
196 | + 'Puerto Rico', | ||
197 | + 'Qatar', | ||
198 | + 'Reunion', | ||
199 | + 'Romania', | ||
200 | + 'Russian Federation', | ||
201 | + 'Rwanda', | ||
202 | + 'Saint Barthélemy', | ||
203 | + 'Saint Helena, Ascension and Tristan da Cunha', | ||
204 | + 'Saint Kitts and Nevis', | ||
205 | + 'Saint Lucia', | ||
206 | + 'Saint Martin (French Part)', | ||
207 | + 'Saint Pierre and Miquelon', | ||
208 | + 'Saint Vincent and the Grenadines', | ||
209 | + 'Samoa', | ||
210 | + 'San Marino', | ||
211 | + 'Sao Tome and Principe', | ||
212 | + 'Saudi Arabia', | ||
213 | + 'Senegal', | ||
214 | + 'Serbia', | ||
215 | + 'Seychelles', | ||
216 | + 'Sierra Leone', | ||
217 | + 'Singapore', | ||
218 | + 'Sint Maarten (Dutch Part)', | ||
219 | + 'Slovakia', | ||
220 | + 'Slovenia', | ||
221 | + 'Solomon Islands', | ||
222 | + 'Somalia', | ||
223 | + 'South Africa', | ||
224 | + 'South Georgia and the South Sandwich Islands', | ||
225 | + 'South Sudan', | ||
226 | + 'Spain', | ||
227 | + 'Sri Lanka', | ||
228 | + 'Sudan', | ||
229 | + 'Suriname', | ||
230 | + 'Svalbard and Jan Mayen', | ||
231 | + 'Swaziland', | ||
232 | + 'Sweden', | ||
233 | + 'Switzerland', | ||
234 | + 'Syrian Arab Republic', | ||
235 | + 'Taiwan', | ||
236 | + 'Tajikistan', | ||
237 | + 'Tanzania, United Republic of', | ||
238 | + 'Thailand', | ||
239 | + 'Timor-Leste', | ||
240 | + 'Togo', | ||
241 | + 'Tokelau', | ||
242 | + 'Tonga', | ||
243 | + 'Trinidad and Tobago', | ||
244 | + 'Tunisia', | ||
245 | + 'Turkey', | ||
246 | + 'Turkmenistan', | ||
247 | + 'Turks and Caicos Islands', | ||
248 | + 'Tuvalu', | ||
249 | + 'Uganda', | ||
250 | + 'Ukraine', | ||
251 | + 'United Arab Emirates', | ||
252 | + 'United Kingdom', | ||
253 | + 'United States', | ||
254 | + 'United States Minor Outlying Islands', | ||
255 | + 'Uruguay', | ||
256 | + 'Uzbekistan', | ||
257 | + 'Vanuatu', | ||
258 | + 'Venezuela', | ||
259 | + 'Viet Nam', | ||
260 | + 'Virgin Islands, British', | ||
261 | + 'Virgin Islands, U.S.', | ||
262 | + 'Wallis and Futuna', | ||
263 | + 'Western Sahara', | ||
264 | + 'Yemen', | ||
265 | + 'Zambia', | ||
266 | + 'Zimbabwe' | ||
267 | +]; | ||
268 | + | ||
269 | +/* tslint:disable */ | ||
270 | +export const POSTAL_CODE_PATTERNS = { | ||
271 | + 'United States': '(\\d{5}([\\-]\\d{4})?)', | ||
272 | + 'Australia': '[0-9]{4}', | ||
273 | + 'Austria': '[0-9]{4}', | ||
274 | + 'Belgium': '[0-9]{4}', | ||
275 | + 'Brazil': '[0-9]{5}[\\-]?[0-9]{3}', | ||
276 | + 'Canada': '^(?!.*[DFIOQU])[A-VXY][0-9][A-Z][ -]?[0-9][A-Z][0-9]$', | ||
277 | + 'Denmark': '[0-9]{3,4}', | ||
278 | + 'Faroe Islands': '[0-9]{3,4}', | ||
279 | + 'Netherlands': '[1-9][0-9]{3}\\s?[a-zA-Z]{2}', | ||
280 | + 'Germany': '[0-9]{5}', | ||
281 | + 'Hungary': '[0-9]{4}', | ||
282 | + 'Italy': '[0-9]{5}', | ||
283 | + 'Japan': '\\d{3}-\\d{4}', | ||
284 | + 'Luxembourg': '(L\\s*(-|—|–))\\s*?[\\d]{4}', | ||
285 | + 'Poland': '[0-9]{2}\\-[0-9]{3}', | ||
286 | + 'Spain': '((0[1-9]|5[0-2])|[1-4][0-9])[0-9]{3}', | ||
287 | + 'Sweden': '\\d{3}\\s?\\d{2}', | ||
288 | + 'United Kingdom': '[A-Za-z]{1,2}[0-9Rr][0-9A-Za-z]? [0-9][ABD-HJLNP-UW-Zabd-hjlnp-uw-z]{2}' | ||
289 | +}; | ||
290 | +/* tslint:enable */ | ||
291 | + |
1 | +<!-- | ||
2 | + | ||
3 | + Copyright © 2016-2019 The Thingsboard Authors | ||
4 | + | ||
5 | + Licensed under the Apache License, Version 2.0 (the "License"); | ||
6 | + you may not use this file except in compliance with the License. | ||
7 | + You may obtain a copy of the License at | ||
8 | + | ||
9 | + http://www.apache.org/licenses/LICENSE-2.0 | ||
10 | + | ||
11 | + Unless required by applicable law or agreed to in writing, software | ||
12 | + distributed under the License is distributed on an "AS IS" BASIS, | ||
13 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
14 | + See the License for the specific language governing permissions and | ||
15 | + limitations under the License. | ||
16 | + | ||
17 | +--> | ||
18 | +<mat-form-field [formGroup]="selectDashboardFormGroup" class="mat-block"> | ||
19 | + <input matInput type="text" placeholder="{{ placeholder || ('dashboard.dashboard' | translate) }}" | ||
20 | + #dashboardInput | ||
21 | + formControlName="dashboard" | ||
22 | + [required]="required" | ||
23 | + [matAutocomplete]="dashboardAutocomplete"> | ||
24 | + <button *ngIf="selectDashboardFormGroup.get('dashboard').value && !disabled" | ||
25 | + type="button" | ||
26 | + matSuffix mat-button mat-icon-button aria-label="Clear" | ||
27 | + (click)="clear()"> | ||
28 | + <mat-icon class="material-icons">close</mat-icon> | ||
29 | + </button> | ||
30 | + <mat-autocomplete #dashboardAutocomplete="matAutocomplete" [displayWith]="displayDashboardFn"> | ||
31 | + <mat-option *ngFor="let dashboard of filteredDashboards | async" [value]="dashboard"> | ||
32 | + <span [innerHTML]="dashboard.title | highlight:searchText"></span> | ||
33 | + </mat-option> | ||
34 | + <mat-option *ngIf="!(filteredDashboards | async)?.length" [value]="null"> | ||
35 | + <span> | ||
36 | + {{ translate.get('dashboard.no-dashboards-matching', {entity: searchText}) | async }} | ||
37 | + </span> | ||
38 | + </mat-option> | ||
39 | + </mat-autocomplete> | ||
40 | + <mat-error> | ||
41 | + <ng-content select="[tb-error]"></ng-content> | ||
42 | + </mat-error> | ||
43 | + <mat-hint> | ||
44 | + <ng-content select="[tb-hint]"></ng-content> | ||
45 | + </mat-hint> | ||
46 | +</mat-form-field> |
1 | +/// | ||
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | ||
3 | +/// | ||
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | +/// you may not use this file except in compliance with the License. | ||
6 | +/// You may obtain a copy of the License at | ||
7 | +/// | ||
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | +/// | ||
10 | +/// Unless required by applicable law or agreed to in writing, software | ||
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | +/// See the License for the specific language governing permissions and | ||
14 | +/// limitations under the License. | ||
15 | +/// | ||
16 | + | ||
17 | +import {AfterViewInit, Component, ElementRef, forwardRef, Input, OnInit, ViewChild} from '@angular/core'; | ||
18 | +import {ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR} from '@angular/forms'; | ||
19 | +import {Observable, of} from 'rxjs'; | ||
20 | +import {PageLink} from '@shared/models/page/page-link'; | ||
21 | +import {Direction} from '@shared/models/page/sort-order'; | ||
22 | +import {map, mergeMap, startWith, tap} from 'rxjs/operators'; | ||
23 | +import {PageData, emptyPageData} from '@shared/models/page/page-data'; | ||
24 | +import {DashboardInfo} from '@app/shared/models/dashboard.models'; | ||
25 | +import {DashboardId} from '@app/shared/models/id/dashboard-id'; | ||
26 | +import {DashboardService} from '@core/http/dashboard.service'; | ||
27 | +import {Store} from '@ngrx/store'; | ||
28 | +import {AppState} from '@app/core/core.state'; | ||
29 | +import {getCurrentAuthUser} from '@app/core/auth/auth.selectors'; | ||
30 | +import {Authority} from '@shared/models/authority.enum'; | ||
31 | +import {TranslateService} from '@ngx-translate/core'; | ||
32 | + | ||
33 | +@Component({ | ||
34 | + selector: 'tb-dashboard-autocomplete', | ||
35 | + templateUrl: './dashboard-autocomplete.component.html', | ||
36 | + styleUrls: [], | ||
37 | + providers: [{ | ||
38 | + provide: NG_VALUE_ACCESSOR, | ||
39 | + useExisting: forwardRef(() => DashboardAutocompleteComponent), | ||
40 | + multi: true | ||
41 | + }] | ||
42 | +}) | ||
43 | +export class DashboardAutocompleteComponent implements ControlValueAccessor, OnInit, AfterViewInit { | ||
44 | + | ||
45 | + selectDashboardFormGroup: FormGroup; | ||
46 | + | ||
47 | + modelValue: DashboardInfo | string | null; | ||
48 | + | ||
49 | + @Input() | ||
50 | + useIdValue = true; | ||
51 | + | ||
52 | + @Input() | ||
53 | + selectFirstDashboard = false; | ||
54 | + | ||
55 | + @Input() | ||
56 | + placeholder: string; | ||
57 | + | ||
58 | + @Input() | ||
59 | + dashboardsScope: 'customer' | 'tenant'; | ||
60 | + | ||
61 | + @Input() | ||
62 | + tenantId: string; | ||
63 | + | ||
64 | + @Input() | ||
65 | + customerId: string; | ||
66 | + | ||
67 | + @Input() | ||
68 | + required: boolean; | ||
69 | + | ||
70 | + @Input() | ||
71 | + disabled: boolean; | ||
72 | + | ||
73 | + @ViewChild('dashboardInput', {static: true}) dashboardInput: ElementRef; | ||
74 | + | ||
75 | + filteredDashboards: Observable<Array<DashboardInfo>>; | ||
76 | + | ||
77 | + private valueLoaded = false; | ||
78 | + | ||
79 | + private searchText = ''; | ||
80 | + | ||
81 | + private propagateChange = (v: any) => { }; | ||
82 | + | ||
83 | + constructor(private store: Store<AppState>, | ||
84 | + public translate: TranslateService, | ||
85 | + private dashboardService: DashboardService, | ||
86 | + private fb: FormBuilder) { | ||
87 | + this.selectDashboardFormGroup = this.fb.group({ | ||
88 | + dashboard: [null] | ||
89 | + }); | ||
90 | + } | ||
91 | + | ||
92 | + registerOnChange(fn: any): void { | ||
93 | + this.propagateChange = fn; | ||
94 | + } | ||
95 | + | ||
96 | + registerOnTouched(fn: any): void { | ||
97 | + } | ||
98 | + | ||
99 | + ngOnInit() { | ||
100 | + | ||
101 | + } | ||
102 | + | ||
103 | + ngAfterViewInit(): void { | ||
104 | + this.selectFirstDashboardIfNeeded(); | ||
105 | + } | ||
106 | + | ||
107 | + selectFirstDashboardIfNeeded(): void { | ||
108 | + if (this.selectFirstDashboard && !this.modelValue) { | ||
109 | + this.getDashboards(new PageLink(1)).subscribe( | ||
110 | + (data) => { | ||
111 | + if (data.data.length) { | ||
112 | + const dashboard = data.data[0]; | ||
113 | + this.modelValue = this.useIdValue ? dashboard.id.id : dashboard; | ||
114 | + this.selectDashboardFormGroup.get('dashboard').patchValue(dashboard, {emitEvent: false}); | ||
115 | + this.propagateChange(this.modelValue); | ||
116 | + } | ||
117 | + } | ||
118 | + ); | ||
119 | + } | ||
120 | + } | ||
121 | + | ||
122 | + setDisabledState(isDisabled: boolean): void { | ||
123 | + this.disabled = isDisabled; | ||
124 | + } | ||
125 | + | ||
126 | + initFilteredResults(): void { | ||
127 | + this.filteredDashboards = this.selectDashboardFormGroup.get('dashboard').valueChanges | ||
128 | + .pipe( | ||
129 | + startWith<string | DashboardInfo>(''), | ||
130 | + tap(value => { | ||
131 | + if (this.valueLoaded) { | ||
132 | + let modelValue; | ||
133 | + if (typeof value === 'string' || !value) { | ||
134 | + modelValue = null; | ||
135 | + } else { | ||
136 | + modelValue = this.useIdValue ? value.id.id : value; | ||
137 | + } | ||
138 | + this.updateView(modelValue); | ||
139 | + } | ||
140 | + }), | ||
141 | + map(value => value ? (typeof value === 'string' ? value : value.name) : ''), | ||
142 | + mergeMap(name => this.fetchDashboards(name) ) | ||
143 | + ); | ||
144 | + } | ||
145 | + | ||
146 | + writeValue(value: DashboardInfo | string | null): void { | ||
147 | + this.valueLoaded = false; | ||
148 | + this.searchText = ''; | ||
149 | + this.initFilteredResults(); | ||
150 | + if (value != null) { | ||
151 | + if (typeof value === 'string') { | ||
152 | + this.dashboardService.getDashboardInfo(value).subscribe( | ||
153 | + (dashboard) => { | ||
154 | + this.modelValue = this.useIdValue ? dashboard.id.id : dashboard; | ||
155 | + this.selectDashboardFormGroup.get('dashboard').patchValue(dashboard, {emitEvent: true}); | ||
156 | + this.valueLoaded = true; | ||
157 | + } | ||
158 | + ); | ||
159 | + } else { | ||
160 | + this.modelValue = this.useIdValue ? value.id.id : value; | ||
161 | + this.selectDashboardFormGroup.get('dashboard').patchValue(value, {emitEvent: false}); | ||
162 | + this.valueLoaded = true; | ||
163 | + } | ||
164 | + } else { | ||
165 | + this.modelValue = null; | ||
166 | + this.selectDashboardFormGroup.get('dashboard').patchValue(null, {emitEvent: false}); | ||
167 | + this.valueLoaded = true; | ||
168 | + } | ||
169 | + } | ||
170 | + | ||
171 | + updateView(value: DashboardInfo | string | null) { | ||
172 | + if (this.modelValue !== value) { | ||
173 | + this.modelValue = value; | ||
174 | + this.propagateChange(this.modelValue); | ||
175 | + } | ||
176 | + } | ||
177 | + | ||
178 | + displayDashboardFn(dashboard?: DashboardInfo): string | undefined { | ||
179 | + return dashboard ? dashboard.title : undefined; | ||
180 | + } | ||
181 | + | ||
182 | + fetchDashboards(searchText?: string): Observable<Array<DashboardInfo>> { | ||
183 | + this.searchText = searchText; | ||
184 | + const pageLink = new PageLink(10, 0, searchText, { | ||
185 | + property: 'title', | ||
186 | + direction: Direction.ASC | ||
187 | + }); | ||
188 | + return this.getDashboards(pageLink).pipe( | ||
189 | + map(pageData => { | ||
190 | + return pageData.data; | ||
191 | + }) | ||
192 | + ); | ||
193 | + } | ||
194 | + | ||
195 | + getDashboards(pageLink: PageLink): Observable<PageData<DashboardInfo>> { | ||
196 | + let dashboardsObservable: Observable<PageData<DashboardInfo>>; | ||
197 | + const authUser = getCurrentAuthUser(this.store); | ||
198 | + if (this.dashboardsScope === 'customer' || authUser.authority === Authority.CUSTOMER_USER) { | ||
199 | + if (this.customerId) { | ||
200 | + dashboardsObservable = this.dashboardService.getCustomerDashboards(this.customerId, pageLink, false, true); | ||
201 | + } else { | ||
202 | + dashboardsObservable = of(emptyPageData()); | ||
203 | + } | ||
204 | + } else { | ||
205 | + if (authUser.authority === Authority.SYS_ADMIN) { | ||
206 | + if (this.tenantId) { | ||
207 | + dashboardsObservable = this.dashboardService.getTenantDashboardsByTenantId(this.tenantId, pageLink, false, true); | ||
208 | + } else { | ||
209 | + dashboardsObservable = of(emptyPageData()); | ||
210 | + } | ||
211 | + } else { | ||
212 | + dashboardsObservable = this.dashboardService.getTenantDashboards(pageLink, false, true); | ||
213 | + } | ||
214 | + } | ||
215 | + return dashboardsObservable; | ||
216 | + } | ||
217 | + | ||
218 | + clear() { | ||
219 | + this.selectDashboardFormGroup.get('dashboard').patchValue(null, {emitEvent: true}); | ||
220 | + setTimeout(() => { | ||
221 | + this.dashboardInput.nativeElement.blur(); | ||
222 | + this.dashboardInput.nativeElement.focus(); | ||
223 | + }, 0); | ||
224 | + } | ||
225 | + | ||
226 | +} |
1 | +<!-- | ||
2 | + | ||
3 | + Copyright © 2016-2019 The Thingsboard Authors | ||
4 | + | ||
5 | + Licensed under the Apache License, Version 2.0 (the "License"); | ||
6 | + you may not use this file except in compliance with the License. | ||
7 | + You may obtain a copy of the License at | ||
8 | + | ||
9 | + http://www.apache.org/licenses/LICENSE-2.0 | ||
10 | + | ||
11 | + Unless required by applicable law or agreed to in writing, software | ||
12 | + distributed under the License is distributed on an "AS IS" BASIS, | ||
13 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
14 | + See the License for the specific language governing permissions and | ||
15 | + limitations under the License. | ||
16 | + | ||
17 | +--> | ||
18 | +<header> | ||
19 | + <mat-toolbar color="primary" [ngStyle]="{height: headerHeightPx+'px'}"> | ||
20 | + <div fxFlex fxLayout="row" fxLayoutAlign="start center"> | ||
21 | + <div class="mat-toolbar-tools" fxFlex fxLayout="column" fxLayoutAlign="start start"> | ||
22 | + <span class="tb-details-title">{{ headerTitle }}</span> | ||
23 | + <span class="tb-details-subtitle">{{ headerSubtitle }}</span> | ||
24 | + <span style="width: 100%;"> | ||
25 | + <ng-content select=".header-pane"></ng-content> | ||
26 | + </span> | ||
27 | + </div> | ||
28 | + <ng-content select=".details-buttons"></ng-content> | ||
29 | + <button mat-button mat-icon-button (click)="onCloseDetails()"> | ||
30 | + <mat-icon class="material-icons">close</mat-icon> | ||
31 | + </button> | ||
32 | + </div> | ||
33 | + <section *ngIf="!isReadOnly" fxLayout="row" class="layout-wrap tb-header-buttons"> | ||
34 | + <button [disabled]="(isLoading$ | async) || theForm.invalid || !theForm.dirty" | ||
35 | + mat-fab | ||
36 | + matTooltip="{{ 'action.apply-changes' | translate }}" | ||
37 | + matTooltipPosition="above" | ||
38 | + color="accent" class="tb-btn-header mat-fab-bottom-right" | ||
39 | + [ngClass]="{'tb-hide': !isEdit}" | ||
40 | + (click)="onApplyDetails()"> | ||
41 | + <mat-icon class="material-icons">done</mat-icon> | ||
42 | + </button> | ||
43 | + <button [disabled]="(isLoading$ | async) || (isAlwaysEdit && !theForm.dirty)" | ||
44 | + mat-fab | ||
45 | + matTooltip="{{ (isAlwaysEdit ? 'action.decline-changes' : 'details.toggle-edit-mode') | translate }}" | ||
46 | + matTooltipPosition="above" | ||
47 | + color="accent" class="tb-btn-header mat-fab-bottom-right" | ||
48 | + (click)="onToggleDetailsEditMode()"> | ||
49 | + <mat-icon class="material-icons">{{isEdit ? 'close' : 'edit'}}</mat-icon> | ||
50 | + </button> | ||
51 | + </section> | ||
52 | + </mat-toolbar> | ||
53 | +</header> | ||
54 | +<div fxFlex class="mat-content"> | ||
55 | + <ng-content></ng-content> | ||
56 | +</div> |
1 | +/** | ||
2 | + * Copyright © 2016-2019 The Thingsboard Authors | ||
3 | + * | ||
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | + * you may not use this file except in compliance with the License. | ||
6 | + * You may obtain a copy of the License at | ||
7 | + * | ||
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | + * | ||
10 | + * Unless required by applicable law or agreed to in writing, software | ||
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | + * See the License for the specific language governing permissions and | ||
14 | + * limitations under the License. | ||
15 | + */ | ||
16 | +@import '../../../scss/constants'; | ||
17 | + | ||
18 | +:host { | ||
19 | + width: 100%; | ||
20 | + height: 100%; | ||
21 | + display: flex; | ||
22 | + flex-direction: column; | ||
23 | + .mat-toolbar-tools { | ||
24 | + height: 100%; | ||
25 | + min-height: 100px; | ||
26 | + max-height: 120px; | ||
27 | + } | ||
28 | + .tb-details-title { | ||
29 | + width: inherit; | ||
30 | + margin: 20px 8px 0 0; | ||
31 | + overflow: hidden; | ||
32 | + font-size: 1rem; | ||
33 | + font-weight: 400; | ||
34 | + text-overflow: ellipsis; | ||
35 | + text-transform: uppercase; | ||
36 | + white-space: nowrap; | ||
37 | + | ||
38 | + @media #{$mat-gt-sm} { | ||
39 | + font-size: 1.6rem; | ||
40 | + } | ||
41 | + } | ||
42 | + | ||
43 | + .tb-details-subtitle { | ||
44 | + width: inherit; | ||
45 | + margin: 10px 0; | ||
46 | + overflow: hidden; | ||
47 | + font-size: 1rem; | ||
48 | + text-overflow: ellipsis; | ||
49 | + white-space: nowrap; | ||
50 | + opacity: .8; | ||
51 | + } | ||
52 | + | ||
53 | +} |
1 | +/// | ||
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | ||
3 | +/// | ||
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | +/// you may not use this file except in compliance with the License. | ||
6 | +/// You may obtain a copy of the License at | ||
7 | +/// | ||
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | +/// | ||
10 | +/// Unless required by applicable law or agreed to in writing, software | ||
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | +/// See the License for the specific language governing permissions and | ||
14 | +/// limitations under the License. | ||
15 | +/// | ||
16 | + | ||
17 | +import { Component, EventEmitter, Input, Output } from '@angular/core'; | ||
18 | +import { PageComponent } from '@shared/components/page.component'; | ||
19 | +import { Store } from '@ngrx/store'; | ||
20 | +import { AppState } from '@core/core.state'; | ||
21 | +import { NgForm } from '@angular/forms'; | ||
22 | + | ||
23 | +@Component({ | ||
24 | + selector: 'tb-details-panel', | ||
25 | + templateUrl: './details-panel.component.html', | ||
26 | + styleUrls: ['./details-panel.component.scss'] | ||
27 | +}) | ||
28 | +export class DetailsPanelComponent extends PageComponent { | ||
29 | + | ||
30 | + @Input() headerHeightPx = 100; | ||
31 | + @Input() headerTitle = ''; | ||
32 | + @Input() headerSubtitle = ''; | ||
33 | + @Input() isReadOnly = false; | ||
34 | + @Input() isAlwaysEdit = false; | ||
35 | + @Input() theForm: NgForm; | ||
36 | + @Output() | ||
37 | + closeDetails = new EventEmitter<void>(); | ||
38 | + @Output() | ||
39 | + toggleDetailsEditMode = new EventEmitter<boolean>(); | ||
40 | + @Output() | ||
41 | + applyDetails = new EventEmitter<void>(); | ||
42 | + | ||
43 | + isEditValue = false; | ||
44 | + | ||
45 | + @Output() | ||
46 | + isEditChange = new EventEmitter<boolean>(); | ||
47 | + | ||
48 | + @Input() | ||
49 | + get isEdit() { | ||
50 | + return this.isEditValue; | ||
51 | + } | ||
52 | + | ||
53 | + set isEdit(val: boolean) { | ||
54 | + this.isEditValue = val; | ||
55 | + this.isEditChange.emit(this.isEditValue); | ||
56 | + } | ||
57 | + | ||
58 | + | ||
59 | + constructor(protected store: Store<AppState>) { | ||
60 | + super(store); | ||
61 | + } | ||
62 | + | ||
63 | + onCloseDetails() { | ||
64 | + this.closeDetails.emit(); | ||
65 | + } | ||
66 | + | ||
67 | + onToggleDetailsEditMode() { | ||
68 | + if (!this.isAlwaysEdit) { | ||
69 | + this.isEdit = !this.isEdit; | ||
70 | + } | ||
71 | + this.toggleDetailsEditMode.emit(this.isEditValue); | ||
72 | + } | ||
73 | + | ||
74 | + onApplyDetails() { | ||
75 | + if (this.theForm.valid) { | ||
76 | + this.applyDetails.emit(); | ||
77 | + } | ||
78 | + } | ||
79 | + | ||
80 | +} |
1 | +<!-- | ||
2 | + | ||
3 | + Copyright © 2016-2019 The Thingsboard Authors | ||
4 | + | ||
5 | + Licensed under the Apache License, Version 2.0 (the "License"); | ||
6 | + you may not use this file except in compliance with the License. | ||
7 | + You may obtain a copy of the License at | ||
8 | + | ||
9 | + http://www.apache.org/licenses/LICENSE-2.0 | ||
10 | + | ||
11 | + Unless required by applicable law or agreed to in writing, software | ||
12 | + distributed under the License is distributed on an "AS IS" BASIS, | ||
13 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
14 | + See the License for the specific language governing permissions and | ||
15 | + limitations under the License. | ||
16 | + | ||
17 | +--> | ||
18 | +<form (ngSubmit)="add()" style="min-width: 400px;"> | ||
19 | + <mat-toolbar fxLayout="row" color="primary"> | ||
20 | + <h2 translate>{{ translations.add }}</h2> | ||
21 | + <span fxFlex></span> | ||
22 | + <div [tb-help]="resources.helpLinkId"></div> | ||
23 | + <button mat-button mat-icon-button | ||
24 | + (click)="cancel()" | ||
25 | + type="button"> | ||
26 | + <mat-icon class="material-icons">close</mat-icon> | ||
27 | + </button> | ||
28 | + </mat-toolbar> | ||
29 | + <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> | ||
30 | + </mat-progress-bar> | ||
31 | + <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div> | ||
32 | + <div mat-dialog-content> | ||
33 | + <tb-anchor #entityDetailsForm></tb-anchor> | ||
34 | + </div> | ||
35 | + <div mat-dialog-actions fxLayout="row"> | ||
36 | + <span fxFlex></span> | ||
37 | + <button mat-button mat-raised-button color="primary" | ||
38 | + type="submit" | ||
39 | + [disabled]="(isLoading$ | async) || detailsForm.invalid || !detailsForm.dirty"> | ||
40 | + {{ 'action.add' | translate }} | ||
41 | + </button> | ||
42 | + <button mat-button color="primary" | ||
43 | + style="margin-right: 20px;" | ||
44 | + type="button" | ||
45 | + cdkFocusInitial | ||
46 | + [disabled]="(isLoading$ | async)" | ||
47 | + (click)="cancel()"> | ||
48 | + {{ 'action.cancel' | translate }} | ||
49 | + </button> | ||
50 | + </div> | ||
51 | +</form> |
1 | +/** | ||
2 | + * Copyright © 2016-2019 The Thingsboard Authors | ||
3 | + * | ||
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | + * you may not use this file except in compliance with the License. | ||
6 | + * You may obtain a copy of the License at | ||
7 | + * | ||
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | + * | ||
10 | + * Unless required by applicable law or agreed to in writing, software | ||
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | + * See the License for the specific language governing permissions and | ||
14 | + * limitations under the License. | ||
15 | + */ | ||
16 | +:host { | ||
17 | +} |
1 | +/// | ||
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | ||
3 | +/// | ||
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | +/// you may not use this file except in compliance with the License. | ||
6 | +/// You may obtain a copy of the License at | ||
7 | +/// | ||
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | +/// | ||
10 | +/// Unless required by applicable law or agreed to in writing, software | ||
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | +/// See the License for the specific language governing permissions and | ||
14 | +/// limitations under the License. | ||
15 | +/// | ||
16 | + | ||
17 | +import { | ||
18 | + Component, | ||
19 | + ComponentFactoryResolver, | ||
20 | + Inject, | ||
21 | + OnInit, | ||
22 | + SkipSelf, | ||
23 | + ViewChild | ||
24 | +} from '@angular/core'; | ||
25 | +import { ErrorStateMatcher, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material'; | ||
26 | +import { PageComponent } from '@shared/components/page.component'; | ||
27 | +import { Store } from '@ngrx/store'; | ||
28 | +import { AppState } from '@core/core.state'; | ||
29 | +import { FormControl, FormGroupDirective, NgForm } from '@angular/forms'; | ||
30 | +import { EntityTypeResource, EntityTypeTranslation } from '@shared/models/entity-type.models'; | ||
31 | +import { EntityTableConfig } from '@shared/components/entity/entities-table-config.models'; | ||
32 | +import { BaseData, HasId } from '@shared/models/base-data'; | ||
33 | +import { EntityId } from '@shared/models/id/entity-id'; | ||
34 | +import { AddEntityDialogData } from '@shared/components/entity/entity-component.models'; | ||
35 | +import { TbAnchorComponent } from '@shared/components/tb-anchor.component'; | ||
36 | +import { EntityComponent } from '@shared/components/entity/entity.component'; | ||
37 | + | ||
38 | +@Component({ | ||
39 | + selector: 'tb-add-entity-dialog', | ||
40 | + templateUrl: './add-entity-dialog.component.html', | ||
41 | + providers: [{provide: ErrorStateMatcher, useExisting: AddEntityDialogComponent}], | ||
42 | + styleUrls: ['./add-entity-dialog.component.scss'] | ||
43 | +}) | ||
44 | +export class AddEntityDialogComponent extends PageComponent implements OnInit, ErrorStateMatcher { | ||
45 | + | ||
46 | + entityComponent: EntityComponent<BaseData<HasId>>; | ||
47 | + detailsForm: NgForm; | ||
48 | + | ||
49 | + entitiesTableConfig: EntityTableConfig<BaseData<HasId>>; | ||
50 | + translations: EntityTypeTranslation; | ||
51 | + resources: EntityTypeResource; | ||
52 | + entity: BaseData<EntityId>; | ||
53 | + | ||
54 | + submitted = false; | ||
55 | + | ||
56 | + @ViewChild('entityDetailsForm', {static: true}) entityDetailsFormAnchor: TbAnchorComponent; | ||
57 | + | ||
58 | + constructor(protected store: Store<AppState>, | ||
59 | + @Inject(MAT_DIALOG_DATA) public data: AddEntityDialogData<BaseData<HasId>>, | ||
60 | + public dialogRef: MatDialogRef<AddEntityDialogComponent, BaseData<HasId>>, | ||
61 | + private componentFactoryResolver: ComponentFactoryResolver, | ||
62 | + @SkipSelf() private errorStateMatcher: ErrorStateMatcher) { | ||
63 | + super(store); | ||
64 | + } | ||
65 | + | ||
66 | + ngOnInit(): void { | ||
67 | + this.entitiesTableConfig = this.data.entitiesTableConfig; | ||
68 | + this.translations = this.entitiesTableConfig.entityTranslations; | ||
69 | + this.resources = this.entitiesTableConfig.entityResources; | ||
70 | + this.entity = {}; | ||
71 | + const componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.entitiesTableConfig.entityComponent); | ||
72 | + const viewContainerRef = this.entityDetailsFormAnchor.viewContainerRef; | ||
73 | + viewContainerRef.clear(); | ||
74 | + const componentRef = viewContainerRef.createComponent(componentFactory); | ||
75 | + this.entityComponent = componentRef.instance; | ||
76 | + this.entityComponent.isEdit = true; | ||
77 | + this.entityComponent.entitiesTableConfig = this.entitiesTableConfig; | ||
78 | + this.entityComponent.entity = this.entity; | ||
79 | + this.detailsForm = this.entityComponent.entityNgForm; | ||
80 | + } | ||
81 | + | ||
82 | + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { | ||
83 | + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); | ||
84 | + const customErrorState = !!(control && control.invalid && this.submitted); | ||
85 | + return originalErrorState || customErrorState; | ||
86 | + } | ||
87 | + | ||
88 | + cancel(): void { | ||
89 | + this.dialogRef.close(null); | ||
90 | + } | ||
91 | + | ||
92 | + add(): void { | ||
93 | + this.submitted = true; | ||
94 | + if (this.detailsForm.valid) { | ||
95 | + this.entity = {...this.entity, ...this.entityComponent.entityFormValue()}; | ||
96 | + this.entitiesTableConfig.saveEntity(this.entity).subscribe( | ||
97 | + (entity) => { | ||
98 | + this.dialogRef.close(entity); | ||
99 | + } | ||
100 | + ); | ||
101 | + } | ||
102 | + } | ||
103 | +} |
1 | +/// | ||
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | ||
3 | +/// | ||
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | +/// you may not use this file except in compliance with the License. | ||
6 | +/// You may obtain a copy of the License at | ||
7 | +/// | ||
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | +/// | ||
10 | +/// Unless required by applicable law or agreed to in writing, software | ||
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | +/// See the License for the specific language governing permissions and | ||
14 | +/// limitations under the License. | ||
15 | +/// | ||
16 | + | ||
17 | +import { Store } from '@ngrx/store'; | ||
18 | +import { AppState } from '@core/core.state'; | ||
19 | +import { EntityComponent } from '@shared/components/entity/entity.component'; | ||
20 | +import { FormBuilder, FormGroup, ValidatorFn, Validators } from '@angular/forms'; | ||
21 | +import { ContactBased } from '@shared/models/contact-based.model'; | ||
22 | +import { AfterViewInit } from '@angular/core'; | ||
23 | +import { POSTAL_CODE_PATTERNS } from '@shared/components/contact.models'; | ||
24 | +import { HasId } from '@shared/models/base-data'; | ||
25 | + | ||
26 | +export abstract class ContactBasedComponent<T extends ContactBased<HasId>> extends EntityComponent<T> implements AfterViewInit { | ||
27 | + | ||
28 | + constructor(protected store: Store<AppState>, | ||
29 | + protected fb: FormBuilder) { | ||
30 | + super(store); | ||
31 | + } | ||
32 | + | ||
33 | + buildForm(entity: T): FormGroup { | ||
34 | + const entityForm = this.buildEntityForm(entity); | ||
35 | + entityForm.addControl('country', this.fb.control(entity ? entity.country : '', [])); | ||
36 | + entityForm.addControl('city', this.fb.control(entity ? entity.city : '', [])); | ||
37 | + entityForm.addControl('state', this.fb.control(entity ? entity.state : '', [])); | ||
38 | + entityForm.addControl('zip', this.fb.control(entity ? entity.zip : '', | ||
39 | + this.zipValidators(entity ? entity.country : '') | ||
40 | + )); | ||
41 | + entityForm.addControl('address', this.fb.control(entity ? entity.address : '', [])); | ||
42 | + entityForm.addControl('address2', this.fb.control(entity ? entity.address2 : '', [])); | ||
43 | + entityForm.addControl('phone', this.fb.control(entity ? entity.phone : '', [])); | ||
44 | + entityForm.addControl('email', this.fb.control(entity ? entity.email : '', [Validators.email])); | ||
45 | + return entityForm; | ||
46 | + } | ||
47 | + | ||
48 | + updateForm(entity: T) { | ||
49 | + this.updateEntityForm(entity); | ||
50 | + this.entityForm.patchValue({country: entity.country}); | ||
51 | + this.entityForm.patchValue({city: entity.city}); | ||
52 | + this.entityForm.patchValue({state: entity.state}); | ||
53 | + this.entityForm.get('zip').setValidators(this.zipValidators(entity.country)); | ||
54 | + this.entityForm.patchValue({zip: entity.zip}); | ||
55 | + this.entityForm.patchValue({address: entity.address}); | ||
56 | + this.entityForm.patchValue({address2: entity.address2}); | ||
57 | + this.entityForm.patchValue({phone: entity.phone}); | ||
58 | + this.entityForm.patchValue({email: entity.email}); | ||
59 | + } | ||
60 | + | ||
61 | + ngAfterViewInit() { | ||
62 | + this.entityForm.get('country').valueChanges.subscribe( | ||
63 | + (country) => { | ||
64 | + this.entityForm.get('zip').setValidators(this.zipValidators(country)); | ||
65 | + this.entityForm.get('zip').updateValueAndValidity({onlySelf: true}); | ||
66 | + this.entityForm.get('zip').markAsTouched({onlySelf: true}); | ||
67 | + } | ||
68 | + ); | ||
69 | + } | ||
70 | + | ||
71 | + zipValidators(country: string): ValidatorFn[] { | ||
72 | + const zipValidators = []; | ||
73 | + if (country && POSTAL_CODE_PATTERNS[country]) { | ||
74 | + const postalCodePattern = POSTAL_CODE_PATTERNS[country]; | ||
75 | + zipValidators.push(Validators.pattern(postalCodePattern)); | ||
76 | + } | ||
77 | + return zipValidators; | ||
78 | + } | ||
79 | + | ||
80 | + abstract buildEntityForm(entity: T): FormGroup; | ||
81 | + | ||
82 | + abstract updateEntityForm(entity: T); | ||
83 | + | ||
84 | +} |
1 | +/// | ||
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | ||
3 | +/// | ||
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | +/// you may not use this file except in compliance with the License. | ||
6 | +/// You may obtain a copy of the License at | ||
7 | +/// | ||
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | +/// | ||
10 | +/// Unless required by applicable law or agreed to in writing, software | ||
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | +/// See the License for the specific language governing permissions and | ||
14 | +/// limitations under the License. | ||
15 | +/// | ||
16 | + | ||
17 | +import { BaseData, HasId } from '@shared/models/base-data'; | ||
18 | +import { EntityId } from '@shared/models/id/entity-id'; | ||
19 | +import { EntitiesFetchFunction } from '@shared/models/datasource/entity-datasource'; | ||
20 | +import { Observable, of } from 'rxjs'; | ||
21 | +import { emptyPageData } from '@shared/models/page/page-data'; | ||
22 | +import { DatePipe } from '@angular/common'; | ||
23 | +import { Direction, SortOrder } from '@shared/models/page/sort-order'; | ||
24 | +import { | ||
25 | + EntityType, | ||
26 | + EntityTypeResource, | ||
27 | + EntityTypeTranslation | ||
28 | +} from '@shared/models/entity-type.models'; | ||
29 | +import { EntityComponent } from '@shared/components/entity/entity.component'; | ||
30 | +import { Type } from '@angular/core'; | ||
31 | +import { EntityAction } from '@shared/components/entity/entity-component.models'; | ||
32 | +import { HasUUID } from '@shared/models/id/has-uuid'; | ||
33 | +import { PageLink } from '@shared/models/page/page-link'; | ||
34 | +import { EntitiesTableComponent } from '@shared/components/entity/entities-table.component'; | ||
35 | +import { EntityTableHeaderComponent } from '@shared/components/entity/entity-table-header.component'; | ||
36 | +import { ActivatedRoute } from '@angular/router'; | ||
37 | + | ||
38 | +export type EntityBooleanFunction<T extends BaseData<HasId>> = (entity: T) => boolean; | ||
39 | +export type EntityStringFunction<T extends BaseData<HasId>> = (entity: T) => string; | ||
40 | +export type EntityCountStringFunction = (count: number) => string; | ||
41 | +export type EntityTwoWayOperation<T extends BaseData<HasId>> = (entity: T) => Observable<T>; | ||
42 | +export type EntityByIdOperation<T extends BaseData<HasId>> = (id: HasUUID) => Observable<T>; | ||
43 | +export type EntityIdOneWayOperation = (id: HasUUID) => Observable<any>; | ||
44 | +export type EntityActionFunction<T extends BaseData<HasId>> = (action: EntityAction<T>) => boolean; | ||
45 | +export type CreateEntityOperation<T extends BaseData<HasId>> = () => Observable<T>; | ||
46 | + | ||
47 | +export type CellContentFunction<T extends BaseData<HasId>> = (entity: T, key: string) => string; | ||
48 | +export type CellStyleFunction<T extends BaseData<HasId>> = (entity: T, key: string) => object; | ||
49 | + | ||
50 | +export interface CellActionDescriptor<T extends BaseData<HasId>> { | ||
51 | + name: string; | ||
52 | + nameFunction?: (entity: T) => string; | ||
53 | + icon?: string; | ||
54 | + isMdiIcon?: boolean; | ||
55 | + color?: string; | ||
56 | + isEnabled: (entity: T) => boolean; | ||
57 | + onAction: ($event: MouseEvent, entity: T) => void; | ||
58 | +} | ||
59 | + | ||
60 | +export interface GroupActionDescriptor<T extends BaseData<HasId>> { | ||
61 | + name: string; | ||
62 | + icon: string; | ||
63 | + isEnabled: boolean; | ||
64 | + onAction: ($event: MouseEvent, entities: T[]) => void; | ||
65 | +} | ||
66 | + | ||
67 | +export interface HeaderActionDescriptor { | ||
68 | + name: string; | ||
69 | + icon: string; | ||
70 | + isEnabled: () => boolean; | ||
71 | + onAction: ($event: MouseEvent) => void; | ||
72 | +} | ||
73 | + | ||
74 | +export class EntityTableColumn<T extends BaseData<HasId>> { | ||
75 | + constructor(public key: string, | ||
76 | + public title: string, | ||
77 | + public maxWidth: string = '100%', | ||
78 | + public cellContentFunction: CellContentFunction<T> = (entity, property) => entity[property], | ||
79 | + public cellStyleFunction: CellStyleFunction<T> = () => ({})) { | ||
80 | + } | ||
81 | +} | ||
82 | + | ||
83 | +export class DateEntityTableColumn<T extends BaseData<HasId>> extends EntityTableColumn<T> { | ||
84 | + constructor(key: string, | ||
85 | + title: string, | ||
86 | + datePipe: DatePipe, | ||
87 | + maxWidth: string = '100%', | ||
88 | + dateFormat: string = 'yyyy-MM-dd HH:mm:ss', | ||
89 | + cellStyleFunction: CellStyleFunction<T> = () => ({})) { | ||
90 | + super(key, | ||
91 | + title, | ||
92 | + maxWidth, | ||
93 | + (entity, property) => datePipe.transform(entity[property], dateFormat), | ||
94 | + cellStyleFunction); | ||
95 | + } | ||
96 | +} | ||
97 | + | ||
98 | +export class EntityTableConfig<T extends BaseData<HasId>, P extends PageLink = PageLink> { | ||
99 | + | ||
100 | + constructor() {} | ||
101 | + | ||
102 | + componentsData: any = null; | ||
103 | + | ||
104 | + loadDataOnInit = true; | ||
105 | + onLoadAction: (route: ActivatedRoute) => void = null; | ||
106 | + table: EntitiesTableComponent = null; | ||
107 | + useTimePageLink = false; | ||
108 | + entityType: EntityType = null; | ||
109 | + tableTitle = ''; | ||
110 | + selectionEnabled = true; | ||
111 | + searchEnabled = true; | ||
112 | + addEnabled = true; | ||
113 | + entitiesDeleteEnabled = true; | ||
114 | + detailsPanelEnabled = true; | ||
115 | + actionsColumnTitle = null; | ||
116 | + entityTranslations: EntityTypeTranslation; | ||
117 | + entityResources: EntityTypeResource; | ||
118 | + entityComponent: Type<EntityComponent<T>>; | ||
119 | + defaultSortOrder: SortOrder = {property: 'createdTime', direction: Direction.ASC}; | ||
120 | + columns: Array<EntityTableColumn<T>> = []; | ||
121 | + cellActionDescriptors: Array<CellActionDescriptor<T>> = []; | ||
122 | + groupActionDescriptors: Array<GroupActionDescriptor<T>> = []; | ||
123 | + headerActionDescriptors: Array<HeaderActionDescriptor> = []; | ||
124 | + headerComponent: Type<EntityTableHeaderComponent<T>>; | ||
125 | + addEntity: CreateEntityOperation<T> = null; | ||
126 | + detailsReadonly: EntityBooleanFunction<T> = () => false; | ||
127 | + deleteEnabled: EntityBooleanFunction<T> = () => true; | ||
128 | + deleteEntityTitle: EntityStringFunction<T> = () => ''; | ||
129 | + deleteEntityContent: EntityStringFunction<T> = () => ''; | ||
130 | + deleteEntitiesTitle: EntityCountStringFunction = () => ''; | ||
131 | + deleteEntitiesContent: EntityCountStringFunction = () => ''; | ||
132 | + loadEntity: EntityByIdOperation<T> = () => of(); | ||
133 | + saveEntity: EntityTwoWayOperation<T> = (entity) => of(entity); | ||
134 | + deleteEntity: EntityIdOneWayOperation = () => of(); | ||
135 | + entitiesFetchFunction: EntitiesFetchFunction<T, P> = () => of(emptyPageData<T>()); | ||
136 | + onEntityAction: EntityActionFunction<T> = () => false; | ||
137 | +} |
1 | +<!-- | ||
2 | + | ||
3 | + Copyright © 2016-2019 The Thingsboard Authors | ||
4 | + | ||
5 | + Licensed under the Apache License, Version 2.0 (the "License"); | ||
6 | + you may not use this file except in compliance with the License. | ||
7 | + You may obtain a copy of the License at | ||
8 | + | ||
9 | + http://www.apache.org/licenses/LICENSE-2.0 | ||
10 | + | ||
11 | + Unless required by applicable law or agreed to in writing, software | ||
12 | + distributed under the License is distributed on an "AS IS" BASIS, | ||
13 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
14 | + See the License for the specific language governing permissions and | ||
15 | + limitations under the License. | ||
16 | + | ||
17 | +--> | ||
18 | +<mat-drawer-container hasBackdrop="false" class="tb-absolute-fill"> | ||
19 | + <mat-drawer *ngIf="entitiesTableConfig.detailsPanelEnabled" | ||
20 | + class="tb-details-drawer mat-elevation-z4" | ||
21 | + #drawer | ||
22 | + mode="over" | ||
23 | + position="end" | ||
24 | + [opened]="isDetailsOpen"> | ||
25 | + <tb-entity-details-panel | ||
26 | + [entitiesTableConfig]="entitiesTableConfig" | ||
27 | + [entityId]="dataSource.currentEntity?.id" | ||
28 | + (closeEntityDetails)="isDetailsOpen = false" | ||
29 | + (entityUpdated)="onEntityUpdated($event)" | ||
30 | + (entityAction)="onEntityAction($event)" | ||
31 | + > | ||
32 | + </tb-entity-details-panel> | ||
33 | + </mat-drawer> | ||
34 | + <mat-drawer-content> | ||
35 | + <div class="mat-padding tb-entity-table tb-absolute-fill"> | ||
36 | + <div fxFlex fxLayout="column" class="mat-elevation-z1 tb-entity-table-content"> | ||
37 | + <mat-toolbar class="mat-table-toolbar" [fxShow]="!textSearchMode && dataSource.selection.isEmpty()"> | ||
38 | + <div class="mat-toolbar-tools"> | ||
39 | + <span *ngIf="entitiesTableConfig.tableTitle" class="tb-entity-table-title">{{ entitiesTableConfig.tableTitle }}</span> | ||
40 | + <tb-timewindow *ngIf="entitiesTableConfig.useTimePageLink" [(ngModel)]="timewindow" | ||
41 | + (ngModelChange)="onTimewindowChange()" | ||
42 | + asButton historyOnly></tb-timewindow> | ||
43 | + <tb-anchor #entityTableHeader></tb-anchor> | ||
44 | + <span fxFlex *ngIf="!this.entitiesTableConfig.headerComponent"></span> | ||
45 | + <button mat-button mat-icon-button [disabled]="isLoading$ | async" [fxShow]="addEnabled()" (click)="addEntity($event)" | ||
46 | + matTooltip="{{ translations.add | translate }}" | ||
47 | + matTooltipPosition="above"> | ||
48 | + <mat-icon>add</mat-icon> | ||
49 | + </button> | ||
50 | + <button mat-button mat-icon-button [disabled]="isLoading$ | async" | ||
51 | + [fxShow]="actionDescriptor.isEnabled()" *ngFor="let actionDescriptor of headerActionDescriptors" | ||
52 | + matTooltip="{{ actionDescriptor.name }}" | ||
53 | + matTooltipPosition="above" | ||
54 | + (click)="actionDescriptor.onAction($event)"> | ||
55 | + <mat-icon>{{actionDescriptor.icon}}</mat-icon> | ||
56 | + </button> | ||
57 | + <button mat-button mat-icon-button [disabled]="isLoading$ | async" (click)="updateData()" | ||
58 | + matTooltip="{{ 'action.refresh' | translate }}" | ||
59 | + matTooltipPosition="above"> | ||
60 | + <mat-icon>refresh</mat-icon> | ||
61 | + </button> | ||
62 | + <button *ngIf="entitiesTableConfig.searchEnabled" | ||
63 | + mat-button mat-icon-button [disabled]="isLoading$ | async" (click)="enterFilterMode()" | ||
64 | + matTooltip="{{ translations.search | translate }}" | ||
65 | + matTooltipPosition="above"> | ||
66 | + <mat-icon>search</mat-icon> | ||
67 | + </button> | ||
68 | + </div> | ||
69 | + </mat-toolbar> | ||
70 | + <mat-toolbar class="mat-table-toolbar" [fxShow]="textSearchMode && dataSource.selection.isEmpty()"> | ||
71 | + <div class="mat-toolbar-tools"> | ||
72 | + <button mat-button mat-icon-button | ||
73 | + matTooltip="{{ translations.search | translate }}" | ||
74 | + matTooltipPosition="above"> | ||
75 | + <mat-icon>search</mat-icon> | ||
76 | + </button> | ||
77 | + <mat-form-field fxFlex> | ||
78 | + <mat-label> </mat-label> | ||
79 | + <input #searchInput matInput | ||
80 | + [(ngModel)]="pageLink.textSearch" | ||
81 | + placeholder="{{ translations.search | translate }}"/> | ||
82 | + </mat-form-field> | ||
83 | + <button mat-button mat-icon-button (click)="exitFilterMode()" | ||
84 | + matTooltip="{{ 'action.close' | translate }}" | ||
85 | + matTooltipPosition="above"> | ||
86 | + <mat-icon>close</mat-icon> | ||
87 | + </button> | ||
88 | + </div> | ||
89 | + </mat-toolbar> | ||
90 | + <mat-toolbar *ngIf="entitiesTableConfig.selectionEnabled" class="mat-table-toolbar" color="primary" [fxShow]="!dataSource.selection.isEmpty()"> | ||
91 | + <div class="mat-toolbar-tools"> | ||
92 | + <span> | ||
93 | + {{ translate.get(translations.selectedEntities, {count: dataSource.selection.selected.length}) | async }} | ||
94 | + </span> | ||
95 | + <span fxFlex></span> | ||
96 | + <button mat-button mat-icon-button [disabled]="isLoading$ | async" | ||
97 | + [fxShow]="actionDescriptor.isEnabled" *ngFor="let actionDescriptor of groupActionDescriptors" | ||
98 | + matTooltip="{{ actionDescriptor.name }}" | ||
99 | + matTooltipPosition="above" | ||
100 | + (click)="actionDescriptor.onAction($event, dataSource.selection.selected)"> | ||
101 | + <mat-icon>{{actionDescriptor.icon}}</mat-icon> | ||
102 | + </button> | ||
103 | + </div> | ||
104 | + </mat-toolbar> | ||
105 | + <div fxFlex class="table-container"> | ||
106 | + <mat-table [dataSource]="dataSource" | ||
107 | + matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="(pageLink.sortOrder.direction + '').toLowerCase()" matSortDisableClear> | ||
108 | + <ng-container matColumnDef="select" sticky> | ||
109 | + <mat-header-cell *matHeaderCellDef> | ||
110 | + <mat-checkbox (change)="$event ? dataSource.masterToggle() : null" | ||
111 | + [checked]="dataSource.selection.hasValue() && (dataSource.isAllSelected() | async)" | ||
112 | + [indeterminate]="dataSource.selection.hasValue() && !(dataSource.isAllSelected() | async)"> | ||
113 | + </mat-checkbox> | ||
114 | + </mat-header-cell> | ||
115 | + <mat-cell *matCellDef="let entity"> | ||
116 | + <mat-checkbox (click)="$event.stopPropagation()" | ||
117 | + (change)="$event ? dataSource.selection.toggle(entity) : null" | ||
118 | + [checked]="dataSource.selection.isSelected(entity)"> | ||
119 | + </mat-checkbox> | ||
120 | + </mat-cell> | ||
121 | + </ng-container> | ||
122 | + <ng-container [matColumnDef]="column.key" *ngFor="let column of columns"> | ||
123 | + <mat-header-cell *matHeaderCellDef [ngStyle]="{maxWidth: column.maxWidth}" mat-sort-header> {{ column.title | translate }} </mat-header-cell> | ||
124 | + <mat-cell *matCellDef="let entity" [ngStyle]="cellStyle(entity, column)" [innerHTML]="cellContent(entity, column)"></mat-cell> | ||
125 | + </ng-container> | ||
126 | + <ng-container matColumnDef="actions" stickyEnd> | ||
127 | + <mat-header-cell *matHeaderCellDef [ngStyle.gt-md]="{ minWidth: (cellActionDescriptors.length * 40) + 'px' }"> | ||
128 | + {{ entitiesTableConfig.actionsColumnTitle ? (entitiesTableConfig.actionsColumnTitle | translate) : '' }} | ||
129 | + </mat-header-cell> | ||
130 | + <mat-cell *matCellDef="let entity" [ngStyle.gt-md]="{ minWidth: (cellActionDescriptors.length * 40) + 'px' }"> | ||
131 | + <div fxHide fxShow.gt-md fxFlex fxLayout="row" fxLayoutAlign="end"> | ||
132 | + <button mat-button mat-icon-button [disabled]="isLoading$ | async" | ||
133 | + [fxShow]="actionDescriptor.isEnabled(entity)" *ngFor="let actionDescriptor of cellActionDescriptors" | ||
134 | + matTooltip="{{ actionDescriptor.nameFunction ? actionDescriptor.nameFunction(entity) : actionDescriptor.name }}" | ||
135 | + matTooltipPosition="above" | ||
136 | + (click)="actionDescriptor.onAction($event, entity)"> | ||
137 | + <mat-icon *ngIf="!actionDescriptor.isMdiIcon" [ngStyle]="actionDescriptor.color ? {color: actionDescriptor.color} : {}"> | ||
138 | + {{actionDescriptor.icon}}</mat-icon> | ||
139 | + <mat-icon *ngIf="actionDescriptor.isMdiIcon" [ngStyle]="actionDescriptor.color ? {color: actionDescriptor.color} : {}" | ||
140 | + [svgIcon]="actionDescriptor.icon"></mat-icon> | ||
141 | + </button> | ||
142 | + </div> | ||
143 | + <div fxHide fxShow.lt-lg> | ||
144 | + <button mat-button mat-icon-button | ||
145 | + (click)="$event.stopPropagation()" | ||
146 | + [matMenuTriggerFor]="cellActionsMenu"> | ||
147 | + <mat-icon class="material-icons">more_vert</mat-icon> | ||
148 | + </button> | ||
149 | + <mat-menu #cellActionsMenu="matMenu" xPosition="before"> | ||
150 | + <button mat-menu-item *ngFor="let actionDescriptor of cellActionDescriptors" | ||
151 | + [disabled]="isLoading$ | async" | ||
152 | + [fxShow]="actionDescriptor.isEnabled(entity)" | ||
153 | + (click)="actionDescriptor.onAction($event, entity)"> | ||
154 | + <mat-icon *ngIf="!actionDescriptor.isMdiIcon" [ngStyle]="actionDescriptor.color ? {color: actionDescriptor.color} : {}"> | ||
155 | + {{actionDescriptor.icon}}</mat-icon> | ||
156 | + <mat-icon *ngIf="actionDescriptor.isMdiIcon" [ngStyle]="actionDescriptor.color ? {color: actionDescriptor.color} : {}" | ||
157 | + [svgIcon]="actionDescriptor.icon"></mat-icon> | ||
158 | + <span>{{ actionDescriptor.nameFunction ? actionDescriptor.nameFunction(entity) : actionDescriptor.name }}</span> | ||
159 | + </button> | ||
160 | + </mat-menu> | ||
161 | + </div> | ||
162 | + </mat-cell> | ||
163 | + </ng-container> | ||
164 | + <mat-header-row [ngClass]="{'mat-row-select': selectionEnabled}" *matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row> | ||
165 | + <mat-row [ngClass]="{'mat-row-select': selectionEnabled, | ||
166 | + 'mat-selected': dataSource.selection.isSelected(entity), | ||
167 | + 'tb-current-entity': dataSource.isCurrentEntity(entity)}" | ||
168 | + *matRowDef="let entity; columns: displayedColumns;" (click)="onRowClick($event, entity)"></mat-row> | ||
169 | + </mat-table> | ||
170 | + <span [fxShow]="dataSource.isEmpty() | async" | ||
171 | + fxLayoutAlign="center center" | ||
172 | + class="no-data-found" translate>{{ translations.noEntities }}</span> | ||
173 | + </div> | ||
174 | + <mat-divider></mat-divider> | ||
175 | + <mat-paginator [length]="dataSource.total() | async" | ||
176 | + [pageIndex]="pageLink.page" | ||
177 | + [pageSize]="pageLink.pageSize" | ||
178 | + [pageSizeOptions]="[10, 20, 30]"></mat-paginator> | ||
179 | + </div> | ||
180 | + </div> | ||
181 | + </mat-drawer-content> | ||
182 | +</mat-drawer-container> |
1 | +/** | ||
2 | + * Copyright © 2016-2019 The Thingsboard Authors | ||
3 | + * | ||
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | + * you may not use this file except in compliance with the License. | ||
6 | + * You may obtain a copy of the License at | ||
7 | + * | ||
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | + * | ||
10 | + * Unless required by applicable law or agreed to in writing, software | ||
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | + * See the License for the specific language governing permissions and | ||
14 | + * limitations under the License. | ||
15 | + */ | ||
16 | +:host { | ||
17 | + width: 100%; | ||
18 | + height: 100%; | ||
19 | + .tb-entity-table { | ||
20 | + .tb-entity-table-content { | ||
21 | + width: 100%; | ||
22 | + height: 100%; | ||
23 | + background: #fff; | ||
24 | + | ||
25 | + .tb-entity-table-title { | ||
26 | + padding-right: 20px; | ||
27 | + white-space: nowrap; | ||
28 | + overflow: hidden; | ||
29 | + text-overflow: ellipsis; | ||
30 | + } | ||
31 | + | ||
32 | + .table-container { | ||
33 | + overflow: auto; | ||
34 | + } | ||
35 | + } | ||
36 | + } | ||
37 | +} | ||
38 | + | ||
39 | +:host ::ng-deep .mat-sort-header-sorted .mat-sort-header-arrow { | ||
40 | + opacity: 1 !important; | ||
41 | +} |
1 | +/// | ||
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | ||
3 | +/// | ||
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | +/// you may not use this file except in compliance with the License. | ||
6 | +/// You may obtain a copy of the License at | ||
7 | +/// | ||
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | +/// | ||
10 | +/// Unless required by applicable law or agreed to in writing, software | ||
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | +/// See the License for the specific language governing permissions and | ||
14 | +/// limitations under the License. | ||
15 | +/// | ||
16 | + | ||
17 | +import { | ||
18 | + AfterViewInit, | ||
19 | + Component, ComponentFactoryResolver, | ||
20 | + ElementRef, | ||
21 | + Input, | ||
22 | + OnInit, | ||
23 | + Type, | ||
24 | + ViewChild | ||
25 | +} from '@angular/core'; | ||
26 | +import { PageComponent } from '@shared/components/page.component'; | ||
27 | +import { Store } from '@ngrx/store'; | ||
28 | +import { AppState } from '@core/core.state'; | ||
29 | +import { PageLink, TimePageLink } from '@shared/models/page/page-link'; | ||
30 | +import { MatDialog, MatPaginator, MatSort } from '@angular/material'; | ||
31 | +import { EntitiesDataSource } from '@shared/models/datasource/entity-datasource'; | ||
32 | +import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators'; | ||
33 | +import { Direction, SortOrder } from '@shared/models/page/sort-order'; | ||
34 | +import { forkJoin, fromEvent, merge, Observable } from 'rxjs'; | ||
35 | +import { TranslateService } from '@ngx-translate/core'; | ||
36 | +import { BaseData, HasId } from '@shared/models/base-data'; | ||
37 | +import { EntityId } from '@shared/models/id/entity-id'; | ||
38 | +import { ActivatedRoute } from '@angular/router'; | ||
39 | +import { | ||
40 | + CellActionDescriptor, | ||
41 | + EntityTableColumn, | ||
42 | + EntityTableConfig, | ||
43 | + GroupActionDescriptor, | ||
44 | + HeaderActionDescriptor | ||
45 | +} from '@shared/components/entity/entities-table-config.models'; | ||
46 | +import { EntityTypeTranslation } from '@shared/models/entity-type.models'; | ||
47 | +import { DialogService } from '@core/services/dialog.service'; | ||
48 | +import { AddEntityDialogComponent } from '@shared/components/entity/add-entity-dialog.component'; | ||
49 | +import { | ||
50 | + AddEntityDialogData, | ||
51 | + EntityAction | ||
52 | +} from '@shared/components/entity/entity-component.models'; | ||
53 | +import { Timewindow } from '@shared/models/time/time.models'; | ||
54 | +import { DomSanitizer } from '@angular/platform-browser'; | ||
55 | +import { TbAnchorComponent } from '@shared/components/tb-anchor.component'; | ||
56 | + | ||
57 | +@Component({ | ||
58 | + selector: 'tb-entities-table', | ||
59 | + templateUrl: './entities-table.component.html', | ||
60 | + styleUrls: ['./entities-table.component.scss'] | ||
61 | +}) | ||
62 | +export class EntitiesTableComponent extends PageComponent implements AfterViewInit, OnInit { | ||
63 | + | ||
64 | + @Input() | ||
65 | + entitiesTableConfig: EntityTableConfig<BaseData<HasId>>; | ||
66 | + | ||
67 | + translations: EntityTypeTranslation; | ||
68 | + | ||
69 | + headerActionDescriptors: Array<HeaderActionDescriptor>; | ||
70 | + groupActionDescriptors: Array<GroupActionDescriptor<BaseData<HasId>>>; | ||
71 | + cellActionDescriptors: Array<CellActionDescriptor<BaseData<HasId>>>; | ||
72 | + | ||
73 | + columns: Array<EntityTableColumn<BaseData<HasId>>>; | ||
74 | + displayedColumns: string[] = []; | ||
75 | + | ||
76 | + selectionEnabled; | ||
77 | + | ||
78 | + pageLink: PageLink; | ||
79 | + textSearchMode = false; | ||
80 | + timewindow: Timewindow; | ||
81 | + dataSource: EntitiesDataSource<BaseData<HasId>>; | ||
82 | + | ||
83 | + isDetailsOpen = false; | ||
84 | + | ||
85 | + @ViewChild('entityTableHeader', {static: false}) entityTableHeaderAnchor: TbAnchorComponent; | ||
86 | + | ||
87 | + @ViewChild('searchInput', {static: false}) searchInputField: ElementRef; | ||
88 | + | ||
89 | + @ViewChild(MatPaginator, {static: false}) paginator: MatPaginator; | ||
90 | + @ViewChild(MatSort, {static: false}) sort: MatSort; | ||
91 | + | ||
92 | + constructor(protected store: Store<AppState>, | ||
93 | + private route: ActivatedRoute, | ||
94 | + public translate: TranslateService, | ||
95 | + public dialog: MatDialog, | ||
96 | + private dialogService: DialogService, | ||
97 | + private domSanitizer: DomSanitizer, | ||
98 | + private componentFactoryResolver: ComponentFactoryResolver) { | ||
99 | + super(store); | ||
100 | + } | ||
101 | + | ||
102 | + ngOnInit() { | ||
103 | + this.entitiesTableConfig = this.entitiesTableConfig || this.route.snapshot.data.entitiesTableConfig; | ||
104 | + if (this.entitiesTableConfig.headerComponent) { | ||
105 | + const componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.entitiesTableConfig.headerComponent); | ||
106 | + const viewContainerRef = this.entityTableHeaderAnchor.viewContainerRef; | ||
107 | + viewContainerRef.clear(); | ||
108 | + const componentRef = viewContainerRef.createComponent(componentFactory); | ||
109 | + const headerComponent = componentRef.instance; | ||
110 | + headerComponent.entitiesTableConfig = this.entitiesTableConfig; | ||
111 | + } | ||
112 | + | ||
113 | + this.entitiesTableConfig.table = this; | ||
114 | + this.translations = this.entitiesTableConfig.entityTranslations; | ||
115 | + | ||
116 | + this.headerActionDescriptors = [...this.entitiesTableConfig.headerActionDescriptors]; | ||
117 | + this.groupActionDescriptors = [...this.entitiesTableConfig.groupActionDescriptors]; | ||
118 | + this.cellActionDescriptors = [...this.entitiesTableConfig.cellActionDescriptors]; | ||
119 | + | ||
120 | + if (this.entitiesTableConfig.entitiesDeleteEnabled) { | ||
121 | + this.cellActionDescriptors.push( | ||
122 | + { | ||
123 | + name: this.translate.instant('action.delete'), | ||
124 | + icon: 'delete', | ||
125 | + isEnabled: entity => this.entitiesTableConfig.deleteEnabled(entity), | ||
126 | + onAction: ($event, entity) => this.deleteEntity($event, entity) | ||
127 | + } | ||
128 | + ); | ||
129 | + } | ||
130 | + | ||
131 | + this.groupActionDescriptors.push( | ||
132 | + { | ||
133 | + name: this.translate.instant('action.delete'), | ||
134 | + icon: 'delete', | ||
135 | + isEnabled: this.entitiesTableConfig.entitiesDeleteEnabled, | ||
136 | + onAction: ($event, entities) => this.deleteEntities($event, entities) | ||
137 | + } | ||
138 | + ); | ||
139 | + | ||
140 | + this.columns = [...this.entitiesTableConfig.columns]; | ||
141 | + | ||
142 | + this.selectionEnabled = this.entitiesTableConfig.selectionEnabled; | ||
143 | + | ||
144 | + if (this.selectionEnabled) { | ||
145 | + this.displayedColumns.push('select'); | ||
146 | + } | ||
147 | + this.columns.forEach( | ||
148 | + (column) => { | ||
149 | + this.displayedColumns.push(column.key); | ||
150 | + } | ||
151 | + ); | ||
152 | + this.displayedColumns.push('actions'); | ||
153 | + | ||
154 | + const sortOrder: SortOrder = { property: this.entitiesTableConfig.defaultSortOrder.property, | ||
155 | + direction: this.entitiesTableConfig.defaultSortOrder.direction }; | ||
156 | + | ||
157 | + if (this.entitiesTableConfig.useTimePageLink) { | ||
158 | + this.timewindow = Timewindow.historyInterval(24 * 60 * 60 * 1000); | ||
159 | + const currentTime = new Date().getTime(); | ||
160 | + this.pageLink = new TimePageLink(10, 0, null, sortOrder, | ||
161 | + currentTime - this.timewindow.history.timewindowMs, currentTime); | ||
162 | + } else { | ||
163 | + this.pageLink = new PageLink(10, 0, null, sortOrder); | ||
164 | + } | ||
165 | + this.dataSource = new EntitiesDataSource<BaseData<HasId>>( | ||
166 | + this.entitiesTableConfig.entitiesFetchFunction | ||
167 | + ); | ||
168 | + if (this.entitiesTableConfig.onLoadAction) { | ||
169 | + this.entitiesTableConfig.onLoadAction(this.route); | ||
170 | + } | ||
171 | + if (this.entitiesTableConfig.loadDataOnInit) { | ||
172 | + this.dataSource.loadEntities(this.pageLink); | ||
173 | + } | ||
174 | + } | ||
175 | + | ||
176 | + ngAfterViewInit() { | ||
177 | + | ||
178 | + fromEvent(this.searchInputField.nativeElement, 'keyup') | ||
179 | + .pipe( | ||
180 | + debounceTime(150), | ||
181 | + distinctUntilChanged(), | ||
182 | + tap(() => { | ||
183 | + this.paginator.pageIndex = 0; | ||
184 | + this.updateData(); | ||
185 | + }) | ||
186 | + ) | ||
187 | + .subscribe(); | ||
188 | + | ||
189 | + this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0); | ||
190 | + | ||
191 | + merge(this.sort.sortChange, this.paginator.page) | ||
192 | + .pipe( | ||
193 | + tap(() => this.updateData()) | ||
194 | + ) | ||
195 | + .subscribe(); | ||
196 | + } | ||
197 | + | ||
198 | + addEnabled() { | ||
199 | + return this.entitiesTableConfig.addEnabled; | ||
200 | + } | ||
201 | + | ||
202 | + updateData(closeDetails: boolean = true) { | ||
203 | + if (closeDetails) { | ||
204 | + this.isDetailsOpen = false; | ||
205 | + } | ||
206 | + this.pageLink.page = this.paginator.pageIndex; | ||
207 | + this.pageLink.pageSize = this.paginator.pageSize; | ||
208 | + this.pageLink.sortOrder.property = this.sort.active; | ||
209 | + this.pageLink.sortOrder.direction = Direction[this.sort.direction.toUpperCase()]; | ||
210 | + if (this.entitiesTableConfig.useTimePageLink) { | ||
211 | + const timePageLink = this.pageLink as TimePageLink; | ||
212 | + if (this.timewindow.history.timewindowMs) { | ||
213 | + const currentTime = new Date().getTime(); | ||
214 | + timePageLink.startTime = currentTime - this.timewindow.history.timewindowMs; | ||
215 | + timePageLink.endTime = currentTime; | ||
216 | + } else { | ||
217 | + timePageLink.startTime = this.timewindow.history.fixedTimewindow.startTimeMs; | ||
218 | + timePageLink.endTime = this.timewindow.history.fixedTimewindow.endTimeMs; | ||
219 | + } | ||
220 | + } | ||
221 | + this.dataSource.loadEntities(this.pageLink); | ||
222 | + } | ||
223 | + | ||
224 | + onRowClick($event: Event, entity) { | ||
225 | + if ($event) { | ||
226 | + $event.stopPropagation(); | ||
227 | + } | ||
228 | + if (this.dataSource.toggleCurrentEntity(entity)) { | ||
229 | + this.isDetailsOpen = true; | ||
230 | + } else { | ||
231 | + this.isDetailsOpen = !this.isDetailsOpen; | ||
232 | + } | ||
233 | + } | ||
234 | + | ||
235 | + addEntity($event: Event) { | ||
236 | + let entity$: Observable<BaseData<HasId>>; | ||
237 | + if (this.entitiesTableConfig.addEntity) { | ||
238 | + entity$ = this.entitiesTableConfig.addEntity(); | ||
239 | + } else { | ||
240 | + entity$ = this.dialog.open<AddEntityDialogComponent, AddEntityDialogData<BaseData<HasId>>, | ||
241 | + BaseData<HasId>>(AddEntityDialogComponent, { | ||
242 | + disableClose: true, | ||
243 | + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], | ||
244 | + data: { | ||
245 | + entitiesTableConfig: this.entitiesTableConfig | ||
246 | + } | ||
247 | + }).afterClosed(); | ||
248 | + } | ||
249 | + entity$.subscribe( | ||
250 | + (entity) => { | ||
251 | + if (entity) { | ||
252 | + this.updateData(); | ||
253 | + } | ||
254 | + } | ||
255 | + ); | ||
256 | + } | ||
257 | + | ||
258 | + onEntityUpdated(entity: BaseData<HasId>) { | ||
259 | + this.updateData(false); | ||
260 | + } | ||
261 | + | ||
262 | + onEntityAction(action: EntityAction<BaseData<HasId>>) { | ||
263 | + if (action.action === 'delete') { | ||
264 | + this.deleteEntity(action.event, action.entity); | ||
265 | + } | ||
266 | + } | ||
267 | + | ||
268 | + deleteEntity($event: Event, entity: BaseData<HasId>) { | ||
269 | + if ($event) { | ||
270 | + $event.stopPropagation(); | ||
271 | + } | ||
272 | + this.dialogService.confirm( | ||
273 | + this.entitiesTableConfig.deleteEntityTitle(entity), | ||
274 | + this.entitiesTableConfig.deleteEntityContent(entity), | ||
275 | + this.translate.instant('action.no'), | ||
276 | + this.translate.instant('action.yes'), | ||
277 | + true | ||
278 | + ).subscribe((result) => { | ||
279 | + if (result) { | ||
280 | + this.entitiesTableConfig.deleteEntity(entity.id).subscribe( | ||
281 | + () => { | ||
282 | + this.updateData(); | ||
283 | + } | ||
284 | + ); | ||
285 | + } | ||
286 | + }); | ||
287 | + } | ||
288 | + | ||
289 | + deleteEntities($event: Event, entities: BaseData<HasId>[]) { | ||
290 | + if ($event) { | ||
291 | + $event.stopPropagation(); | ||
292 | + } | ||
293 | + this.dialogService.confirm( | ||
294 | + this.entitiesTableConfig.deleteEntitiesTitle(entities.length), | ||
295 | + this.entitiesTableConfig.deleteEntitiesContent(entities.length), | ||
296 | + this.translate.instant('action.no'), | ||
297 | + this.translate.instant('action.yes'), | ||
298 | + true | ||
299 | + ).subscribe((result) => { | ||
300 | + if (result) { | ||
301 | + const tasks: Observable<any>[] = []; | ||
302 | + entities.forEach((entity) => { | ||
303 | + if (this.entitiesTableConfig.deleteEnabled(entity)) { | ||
304 | + tasks.push(this.entitiesTableConfig.deleteEntity(entity.id)); | ||
305 | + } | ||
306 | + }); | ||
307 | + forkJoin(tasks).subscribe( | ||
308 | + () => { | ||
309 | + this.updateData(); | ||
310 | + } | ||
311 | + ); | ||
312 | + } | ||
313 | + }); | ||
314 | + } | ||
315 | + | ||
316 | + onTimewindowChange() { | ||
317 | + this.updateData(); | ||
318 | + } | ||
319 | + | ||
320 | + enterFilterMode() { | ||
321 | + this.textSearchMode = true; | ||
322 | + this.pageLink.textSearch = ''; | ||
323 | + setTimeout(() => { | ||
324 | + this.searchInputField.nativeElement.focus(); | ||
325 | + this.searchInputField.nativeElement.setSelectionRange(0, 0); | ||
326 | + }, 10); | ||
327 | + } | ||
328 | + | ||
329 | + exitFilterMode() { | ||
330 | + this.textSearchMode = false; | ||
331 | + this.pageLink.textSearch = null; | ||
332 | + this.paginator.pageIndex = 0; | ||
333 | + this.updateData(); | ||
334 | + } | ||
335 | + | ||
336 | + resetSortAndFilter(update: boolean = true) { | ||
337 | + this.pageLink.textSearch = null; | ||
338 | + if (this.entitiesTableConfig.useTimePageLink) { | ||
339 | + this.timewindow = Timewindow.historyInterval(24 * 60 * 60 * 1000); | ||
340 | + } | ||
341 | + this.paginator.pageIndex = 0; | ||
342 | + const sortable = this.sort.sortables.get(this.entitiesTableConfig.defaultSortOrder.property); | ||
343 | + this.sort.active = sortable.id; | ||
344 | + this.sort.direction = this.entitiesTableConfig.defaultSortOrder.direction === Direction.ASC ? 'asc' : 'desc'; | ||
345 | + if (update) { | ||
346 | + this.updateData(); | ||
347 | + } | ||
348 | + } | ||
349 | + | ||
350 | + cellContent(entity: BaseData<HasId>, column: EntityTableColumn<BaseData<HasId>>) { | ||
351 | + return this.domSanitizer.bypassSecurityTrustHtml(column.cellContentFunction(entity, column.key)); | ||
352 | + } | ||
353 | + | ||
354 | + cellStyle(entity: BaseData<HasId>, column: EntityTableColumn<BaseData<HasId>>) { | ||
355 | + return {...column.cellStyleFunction(entity, column.key), ...{maxWidth: column.maxWidth}}; | ||
356 | + } | ||
357 | + | ||
358 | +} |
1 | +/// | ||
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | ||
3 | +/// | ||
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | +/// you may not use this file except in compliance with the License. | ||
6 | +/// You may obtain a copy of the License at | ||
7 | +/// | ||
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | +/// | ||
10 | +/// Unless required by applicable law or agreed to in writing, software | ||
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | +/// See the License for the specific language governing permissions and | ||
14 | +/// limitations under the License. | ||
15 | +/// | ||
16 | + | ||
17 | +import { BaseData, HasId } from '@shared/models/base-data'; | ||
18 | +import { EntityTableConfig } from '@shared/components/entity/entities-table-config.models'; | ||
19 | + | ||
20 | +export interface AddEntityDialogData<T extends BaseData<HasId>> { | ||
21 | + entitiesTableConfig: EntityTableConfig<T>; | ||
22 | +} | ||
23 | + | ||
24 | +export interface EntityAction<T extends BaseData<HasId>> { | ||
25 | + event: Event; | ||
26 | + action: string; | ||
27 | + entity: T; | ||
28 | +} |
1 | +<!-- | ||
2 | + | ||
3 | + Copyright © 2016-2019 The Thingsboard Authors | ||
4 | + | ||
5 | + Licensed under the Apache License, Version 2.0 (the "License"); | ||
6 | + you may not use this file except in compliance with the License. | ||
7 | + You may obtain a copy of the License at | ||
8 | + | ||
9 | + http://www.apache.org/licenses/LICENSE-2.0 | ||
10 | + | ||
11 | + Unless required by applicable law or agreed to in writing, software | ||
12 | + distributed under the License is distributed on an "AS IS" BASIS, | ||
13 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
14 | + See the License for the specific language governing permissions and | ||
15 | + limitations under the License. | ||
16 | + | ||
17 | +--> | ||
18 | +<tb-details-panel fxFlex | ||
19 | + [headerTitle]="entity?.name" | ||
20 | + headerSubtitle="{{ translations.details | translate }}" | ||
21 | + [isReadOnly]="entitiesTableConfig.detailsReadonly(entity)" | ||
22 | + [isEdit]="isEditValue" | ||
23 | + (closeDetails)="onCloseEntityDetails()" | ||
24 | + (toggleDetailsEditMode)="onToggleEditMode($event)" | ||
25 | + (applyDetails)="saveEntity()" | ||
26 | + [theForm]="detailsForm"> | ||
27 | + <div class="details-buttons"> | ||
28 | + <div [tb-help]="resources.helpLinkId"></div> | ||
29 | + </div> | ||
30 | + <mat-tab-group class="tb-absolute-fill" [ngClass]="{'tb-headless': isEditValue}" fxFlex [(selectedIndex)]="selectedTab"> | ||
31 | + <mat-tab label="{{ 'details.details' | translate }}"> | ||
32 | + <tb-anchor #entityDetailsForm></tb-anchor> | ||
33 | + </mat-tab> | ||
34 | + <!--mat-tab *ngIf="entity && entitiesTableConfig.entityType !== entityTypes.CUSTOMER" | ||
35 | + label="{{ 'audit-log.audit-logs' | translate }}"> | ||
36 | + <tb-audit-log-table [active]="selectedTab === 1" [auditLogMode]="auditLogModes.ENTITY" [entityId]="entity.id" detailsMode="true"></tb-audit-log-table> | ||
37 | + </mat-tab> | ||
38 | + <mat-tab *ngIf="entity && entitiesTableConfig.entityType === entityTypes.CUSTOMER" | ||
39 | + label="{{ 'audit-log.audit-logs' | translate }}"> | ||
40 | + <tb-audit-log-table [active]="selectedTab === 1" [auditLogMode]="auditLogModes.CUSTOMER" [customerId]="entity.id" detailsMode="true"></tb-audit-log-table> | ||
41 | + </mat-tab--> | ||
42 | + </mat-tab-group> | ||
43 | +</tb-details-panel> |
1 | +/** | ||
2 | + * Copyright © 2016-2019 The Thingsboard Authors | ||
3 | + * | ||
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | + * you may not use this file except in compliance with the License. | ||
6 | + * You may obtain a copy of the License at | ||
7 | + * | ||
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | + * | ||
10 | + * Unless required by applicable law or agreed to in writing, software | ||
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | + * See the License for the specific language governing permissions and | ||
14 | + * limitations under the License. | ||
15 | + */ | ||
16 | +:host { | ||
17 | + width: 100%; | ||
18 | + height: 100%; | ||
19 | + display: flex; | ||
20 | + flex-direction: column; | ||
21 | +} | ||
22 | + | ||
23 | +:host ::ng-deep { | ||
24 | + .mat-tab-body-wrapper { | ||
25 | + position: absolute; | ||
26 | + top: 49px; | ||
27 | + left: 0; | ||
28 | + right: 0; | ||
29 | + bottom: 0; | ||
30 | + } | ||
31 | +} |
1 | +/// | ||
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | ||
3 | +/// | ||
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | +/// you may not use this file except in compliance with the License. | ||
6 | +/// You may obtain a copy of the License at | ||
7 | +/// | ||
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | +/// | ||
10 | +/// Unless required by applicable law or agreed to in writing, software | ||
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | +/// See the License for the specific language governing permissions and | ||
14 | +/// limitations under the License. | ||
15 | +/// | ||
16 | + | ||
17 | +import { | ||
18 | + Component, | ||
19 | + ComponentFactoryResolver, | ||
20 | + EventEmitter, | ||
21 | + Input, | ||
22 | + OnDestroy, | ||
23 | + OnInit, | ||
24 | + Output, | ||
25 | + ViewChild | ||
26 | +} from '@angular/core'; | ||
27 | +import { PageComponent } from '@shared/components/page.component'; | ||
28 | +import { Store } from '@ngrx/store'; | ||
29 | +import { AppState } from '@core/core.state'; | ||
30 | +import { EntityTableConfig } from '@shared/components/entity/entities-table-config.models'; | ||
31 | +import { BaseData, HasId } from '@shared/models/base-data'; | ||
32 | +import { | ||
33 | + EntityType, | ||
34 | + EntityTypeResource, | ||
35 | + EntityTypeTranslation | ||
36 | +} from '@shared/models/entity-type.models'; | ||
37 | +import { NgForm } from '@angular/forms'; | ||
38 | +import { EntityComponent } from '@shared/components/entity/entity.component'; | ||
39 | +import { TbAnchorComponent } from '@shared/components/tb-anchor.component'; | ||
40 | +import { EntityAction } from '@shared/components/entity/entity-component.models'; | ||
41 | +import { Subscription } from 'rxjs'; | ||
42 | +// import { AuditLogMode } from '@shared/models/audit-log.models'; | ||
43 | + | ||
44 | +@Component({ | ||
45 | + selector: 'tb-entity-details-panel', | ||
46 | + templateUrl: './entity-details-panel.component.html', | ||
47 | + styleUrls: ['./entity-details-panel.component.scss'] | ||
48 | +}) | ||
49 | +export class EntityDetailsPanelComponent extends PageComponent implements OnInit, OnDestroy { | ||
50 | + | ||
51 | + @Input() entitiesTableConfig: EntityTableConfig<BaseData<HasId>>; | ||
52 | + | ||
53 | + @Output() | ||
54 | + closeEntityDetails = new EventEmitter<void>(); | ||
55 | + | ||
56 | + @Output() | ||
57 | + entityUpdated = new EventEmitter<BaseData<HasId>>(); | ||
58 | + | ||
59 | + @Output() | ||
60 | + entityAction = new EventEmitter<EntityAction<BaseData<HasId>>>(); | ||
61 | + | ||
62 | + entityComponent: EntityComponent<BaseData<HasId>>; | ||
63 | + detailsForm: NgForm; | ||
64 | + | ||
65 | + isEditValue = false; | ||
66 | + selectedTab = 0; | ||
67 | + | ||
68 | + entityTypes = EntityType; | ||
69 | + | ||
70 | + @ViewChild('entityDetailsForm', {static: true}) entityDetailsFormAnchor: TbAnchorComponent; | ||
71 | + | ||
72 | + translations: EntityTypeTranslation; | ||
73 | + resources: EntityTypeResource; | ||
74 | + entity: BaseData<HasId>; | ||
75 | + | ||
76 | + private currentEntityId: HasId; | ||
77 | + private entityActionSubscription: Subscription; | ||
78 | + | ||
79 | + constructor(protected store: Store<AppState>, | ||
80 | + private componentFactoryResolver: ComponentFactoryResolver) { | ||
81 | + super(store); | ||
82 | + } | ||
83 | + | ||
84 | + @Input() | ||
85 | + set entityId(entityId: HasId) { | ||
86 | + if (entityId && entityId !== this.currentEntityId) { | ||
87 | + this.currentEntityId = entityId; | ||
88 | + this.reload(); | ||
89 | + } | ||
90 | + } | ||
91 | + | ||
92 | + set isEdit(val: boolean) { | ||
93 | + this.isEditValue = val; | ||
94 | + this.entityComponent.isEdit = val; | ||
95 | + } | ||
96 | + | ||
97 | + get isEdit() { | ||
98 | + return this.isEditValue; | ||
99 | + } | ||
100 | + | ||
101 | + ngOnInit(): void { | ||
102 | + this.translations = this.entitiesTableConfig.entityTranslations; | ||
103 | + this.resources = this.entitiesTableConfig.entityResources; | ||
104 | + this.buildEntityComponent(); | ||
105 | + } | ||
106 | + | ||
107 | + ngOnDestroy(): void { | ||
108 | + super.ngOnDestroy(); | ||
109 | + if (this.entityActionSubscription) { | ||
110 | + this.entityActionSubscription.unsubscribe(); | ||
111 | + } | ||
112 | + } | ||
113 | + | ||
114 | + buildEntityComponent() { | ||
115 | + const componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.entitiesTableConfig.entityComponent); | ||
116 | + const viewContainerRef = this.entityDetailsFormAnchor.viewContainerRef; | ||
117 | + viewContainerRef.clear(); | ||
118 | + const componentRef = viewContainerRef.createComponent(componentFactory); | ||
119 | + this.entityComponent = componentRef.instance; | ||
120 | + this.entityComponent.isEdit = this.isEdit; | ||
121 | + this.entityComponent.entitiesTableConfig = this.entitiesTableConfig; | ||
122 | + this.detailsForm = this.entityComponent.entityNgForm; | ||
123 | + this.entityActionSubscription = this.entityComponent.entityAction.subscribe((action) => { | ||
124 | + this.entityAction.emit(action); | ||
125 | + }); | ||
126 | + } | ||
127 | + | ||
128 | + reload(): void { | ||
129 | + this.isEdit = false; | ||
130 | + this.entitiesTableConfig.loadEntity(this.currentEntityId).subscribe( | ||
131 | + (entity) => { | ||
132 | + this.entity = entity; | ||
133 | + this.entityComponent.entity = entity; | ||
134 | + } | ||
135 | + ); | ||
136 | + } | ||
137 | + | ||
138 | + onCloseEntityDetails() { | ||
139 | + this.closeEntityDetails.emit(); | ||
140 | + } | ||
141 | + | ||
142 | + onToggleEditMode(isEdit: boolean) { | ||
143 | + this.isEdit = isEdit; | ||
144 | + if (!this.isEdit) { | ||
145 | + this.entityComponent.entity = this.entity; | ||
146 | + } else { | ||
147 | + this.selectedTab = 0; | ||
148 | + } | ||
149 | + } | ||
150 | + | ||
151 | + saveEntity() { | ||
152 | + if (this.detailsForm.valid) { | ||
153 | + const editingEntity = {...this.entity, ...this.entityComponent.entityFormValue()}; | ||
154 | + this.entitiesTableConfig.saveEntity(editingEntity).subscribe( | ||
155 | + (entity) => { | ||
156 | + this.entity = entity; | ||
157 | + this.entityComponent.entity = entity; | ||
158 | + this.isEdit = false; | ||
159 | + this.entityUpdated.emit(this.entity); | ||
160 | + } | ||
161 | + ); | ||
162 | + } | ||
163 | + } | ||
164 | + | ||
165 | +} |
1 | +/// | ||
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | ||
3 | +/// | ||
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | +/// you may not use this file except in compliance with the License. | ||
6 | +/// You may obtain a copy of the License at | ||
7 | +/// | ||
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | +/// | ||
10 | +/// Unless required by applicable law or agreed to in writing, software | ||
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | +/// See the License for the specific language governing permissions and | ||
14 | +/// limitations under the License. | ||
15 | +/// | ||
16 | + | ||
17 | +import { BaseData, HasId } from '@shared/models/base-data'; | ||
18 | +import { PageComponent } from '@shared/components/page.component'; | ||
19 | +import { Input, OnInit } from '@angular/core'; | ||
20 | +import { Store } from '@ngrx/store'; | ||
21 | +import { AppState } from '@core/core.state'; | ||
22 | +import { EntityTableConfig } from '@shared/components/entity/entities-table-config.models'; | ||
23 | + | ||
24 | +export abstract class EntityTableHeaderComponent<T extends BaseData<HasId>> extends PageComponent implements OnInit { | ||
25 | + | ||
26 | + @Input() | ||
27 | + entitiesTableConfig: EntityTableConfig<T>; | ||
28 | + | ||
29 | + protected constructor(protected store: Store<AppState>) { | ||
30 | + super(store); | ||
31 | + } | ||
32 | + | ||
33 | + ngOnInit() { | ||
34 | + } | ||
35 | + | ||
36 | +} |
1 | +/// | ||
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | ||
3 | +/// | ||
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | +/// you may not use this file except in compliance with the License. | ||
6 | +/// You may obtain a copy of the License at | ||
7 | +/// | ||
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | +/// | ||
10 | +/// Unless required by applicable law or agreed to in writing, software | ||
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | +/// See the License for the specific language governing permissions and | ||
14 | +/// limitations under the License. | ||
15 | +/// | ||
16 | + | ||
17 | +import { BaseData, HasId } from '@shared/models/base-data'; | ||
18 | +import { FormGroup, NgForm } from '@angular/forms'; | ||
19 | +import { PageComponent } from '@shared/components/page.component'; | ||
20 | +import { EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'; | ||
21 | +import { Store } from '@ngrx/store'; | ||
22 | +import { AppState } from '@core/core.state'; | ||
23 | +import { EntityAction } from '@shared/components/entity/entity-component.models'; | ||
24 | +import { EntityTableConfig } from '@shared/components/entity/entities-table-config.models'; | ||
25 | + | ||
26 | +export abstract class EntityComponent<T extends BaseData<HasId>> extends PageComponent implements OnInit { | ||
27 | + | ||
28 | + entityValue: T; | ||
29 | + entityForm: FormGroup; | ||
30 | + | ||
31 | + @ViewChild('entityNgForm', {static: true}) entityNgForm: NgForm; | ||
32 | + | ||
33 | + isEditValue: boolean; | ||
34 | + | ||
35 | + @Input() | ||
36 | + set isEdit(isEdit: boolean) { | ||
37 | + this.isEditValue = isEdit; | ||
38 | + this.updateFormState(); | ||
39 | + } | ||
40 | + | ||
41 | + get isEdit() { | ||
42 | + return this.isEditValue; | ||
43 | + } | ||
44 | + | ||
45 | + get isAdd(): boolean { | ||
46 | + return this.entityValue && !this.entityValue.id; | ||
47 | + } | ||
48 | + | ||
49 | + @Input() | ||
50 | + set entity(entity: T) { | ||
51 | + this.entityValue = entity; | ||
52 | + if (this.entityForm) { | ||
53 | + this.entityForm.reset(); | ||
54 | + this.updateForm(entity); | ||
55 | + } | ||
56 | + } | ||
57 | + | ||
58 | + get entity(): T { | ||
59 | + return this.entityValue; | ||
60 | + } | ||
61 | + | ||
62 | + @Input() | ||
63 | + entitiesTableConfig: EntityTableConfig<T>; | ||
64 | + | ||
65 | + @Output() | ||
66 | + entityAction = new EventEmitter<EntityAction<T>>(); | ||
67 | + | ||
68 | + protected constructor(protected store: Store<AppState>) { | ||
69 | + super(store); | ||
70 | + } | ||
71 | + | ||
72 | + ngOnInit() { | ||
73 | + this.entityForm = this.buildForm(this.entityValue); | ||
74 | + } | ||
75 | + | ||
76 | + onEntityAction($event: Event, action: string) { | ||
77 | + const entityAction = {event: $event, action, entity: this.entity} as EntityAction<T>; | ||
78 | + let handled = false; | ||
79 | + if (this.entitiesTableConfig) { | ||
80 | + handled = this.entitiesTableConfig.onEntityAction(entityAction); | ||
81 | + } | ||
82 | + if (!handled) { | ||
83 | + this.entityAction.emit(entityAction); | ||
84 | + } | ||
85 | + } | ||
86 | + | ||
87 | + updateFormState() { | ||
88 | + if (this.entityForm) { | ||
89 | + if (this.isEditValue) { | ||
90 | + this.entityForm.enable({emitEvent: false}); | ||
91 | + } else { | ||
92 | + this.entityForm.disable({emitEvent: false}); | ||
93 | + } | ||
94 | + } | ||
95 | + } | ||
96 | + | ||
97 | + entityFormValue() { | ||
98 | + const formValue = this.entityForm ? {...this.entityForm.value} : {}; | ||
99 | + return this.prepareFormValue(formValue); | ||
100 | + } | ||
101 | + | ||
102 | + prepareFormValue(formValue: any): any { | ||
103 | + return formValue; | ||
104 | + } | ||
105 | + | ||
106 | + abstract buildForm(entity: T): FormGroup; | ||
107 | + | ||
108 | + abstract updateForm(entity: T); | ||
109 | + | ||
110 | +} |
1 | +<!-- | ||
2 | + | ||
3 | + Copyright © 2016-2019 The Thingsboard Authors | ||
4 | + | ||
5 | + Licensed under the Apache License, Version 2.0 (the "License"); | ||
6 | + you may not use this file except in compliance with the License. | ||
7 | + You may obtain a copy of the License at | ||
8 | + | ||
9 | + http://www.apache.org/licenses/LICENSE-2.0 | ||
10 | + | ||
11 | + Unless required by applicable law or agreed to in writing, software | ||
12 | + distributed under the License is distributed on an "AS IS" BASIS, | ||
13 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
14 | + See the License for the specific language governing permissions and | ||
15 | + limitations under the License. | ||
16 | + | ||
17 | +--> | ||
18 | +<section fxLayout="column" fxLayoutAlign="start start"> | ||
19 | + <section fxLayout="row" fxLayoutAlign="start start" fxLayoutGap="16px"> | ||
20 | + <mat-form-field> | ||
21 | + <mat-placeholder translate>datetime.date-from</mat-placeholder> | ||
22 | + <mat-datetimepicker-toggle [for]="startDatePicker" matPrefix></mat-datetimepicker-toggle> | ||
23 | + <mat-datetimepicker #startDatePicker type="date" openOnFocus="true"></mat-datetimepicker> | ||
24 | + <input matInput [(ngModel)]="startDate" [matDatetimepicker]="startDatePicker" (ngModelChange)="onStartDateChange()"> | ||
25 | + </mat-form-field> | ||
26 | + <mat-form-field> | ||
27 | + <mat-placeholder translate>datetime.time-from</mat-placeholder> | ||
28 | + <mat-datetimepicker-toggle [for]="startTimePicker" matPrefix></mat-datetimepicker-toggle> | ||
29 | + <mat-datetimepicker #startTimePicker type="time" openOnFocus="true"></mat-datetimepicker> | ||
30 | + <input matInput [(ngModel)]="startDate" [matDatetimepicker]="startTimePicker" (ngModelChange)="onStartDateChange()"> | ||
31 | + </mat-form-field> | ||
32 | + </section> | ||
33 | + <section fxLayout="row" fxLayoutAlign="start start" fxLayoutGap="16px"> | ||
34 | + <mat-form-field> | ||
35 | + <mat-placeholder translate>datetime.date-to</mat-placeholder> | ||
36 | + <mat-datetimepicker-toggle [for]="endDatePicker" matPrefix></mat-datetimepicker-toggle> | ||
37 | + <mat-datetimepicker #endDatePicker type="date" openOnFocus="true"></mat-datetimepicker> | ||
38 | + <input matInput [(ngModel)]="endDate" [matDatetimepicker]="endDatePicker" (ngModelChange)="onEndDateChange()"> | ||
39 | + </mat-form-field> | ||
40 | + <mat-form-field> | ||
41 | + <mat-placeholder translate>datetime.time-to</mat-placeholder> | ||
42 | + <mat-datetimepicker-toggle [for]="endTimePicker" matPrefix></mat-datetimepicker-toggle> | ||
43 | + <mat-datetimepicker #endTimePicker type="time" openOnFocus="true"></mat-datetimepicker> | ||
44 | + <input matInput [(ngModel)]="endDate" [matDatetimepicker]="endTimePicker" (ngModelChange)="onEndDateChange()"> | ||
45 | + </mat-form-field> | ||
46 | + </section> | ||
47 | +</section> |
1 | +/** | ||
2 | + * Copyright © 2016-2019 The Thingsboard Authors | ||
3 | + * | ||
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | + * you may not use this file except in compliance with the License. | ||
6 | + * You may obtain a copy of the License at | ||
7 | + * | ||
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | + * | ||
10 | + * Unless required by applicable law or agreed to in writing, software | ||
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | + * See the License for the specific language governing permissions and | ||
14 | + * limitations under the License. | ||
15 | + */ | ||
16 | +:host ::ng-deep { | ||
17 | + .mat-form-field-wrapper { | ||
18 | + padding-bottom: 8px; | ||
19 | + } | ||
20 | + .mat-form-field-underline { | ||
21 | + bottom: 8px; | ||
22 | + } | ||
23 | + .mat-form-field-infix { | ||
24 | + width: 150px; | ||
25 | + } | ||
26 | +} |
1 | +/// | ||
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | ||
3 | +/// | ||
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | +/// you may not use this file except in compliance with the License. | ||
6 | +/// You may obtain a copy of the License at | ||
7 | +/// | ||
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | +/// | ||
10 | +/// Unless required by applicable law or agreed to in writing, software | ||
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | +/// See the License for the specific language governing permissions and | ||
14 | +/// limitations under the License. | ||
15 | +/// | ||
16 | + | ||
17 | +import { Component, forwardRef, Input, OnInit } from '@angular/core'; | ||
18 | +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; | ||
19 | +import { FixedWindow } from '@shared/models/time/time.models'; | ||
20 | + | ||
21 | +@Component({ | ||
22 | + selector: 'tb-datetime-period', | ||
23 | + templateUrl: './datetime-period.component.html', | ||
24 | + styleUrls: ['./datetime-period.component.scss'], | ||
25 | + providers: [ | ||
26 | + { | ||
27 | + provide: NG_VALUE_ACCESSOR, | ||
28 | + useExisting: forwardRef(() => DatetimePeriodComponent), | ||
29 | + multi: true | ||
30 | + } | ||
31 | + ] | ||
32 | +}) | ||
33 | +export class DatetimePeriodComponent implements OnInit, ControlValueAccessor { | ||
34 | + | ||
35 | + @Input() disabled: boolean; | ||
36 | + | ||
37 | + modelValue: FixedWindow; | ||
38 | + | ||
39 | + startDate: Date; | ||
40 | + endDate: Date; | ||
41 | + | ||
42 | + endTime: any; | ||
43 | + | ||
44 | + maxStartDate: Date; | ||
45 | + minEndDate: Date; | ||
46 | + maxEndDate: Date; | ||
47 | + | ||
48 | + changePending = false; | ||
49 | + | ||
50 | + private propagateChange = null; | ||
51 | + | ||
52 | + constructor() { | ||
53 | + } | ||
54 | + | ||
55 | + ngOnInit(): void { | ||
56 | + } | ||
57 | + | ||
58 | + registerOnChange(fn: any): void { | ||
59 | + this.propagateChange = fn; | ||
60 | + if (this.changePending && this.propagateChange) { | ||
61 | + this.changePending = false; | ||
62 | + this.propagateChange(this.modelValue); | ||
63 | + } | ||
64 | + } | ||
65 | + | ||
66 | + registerOnTouched(fn: any): void { | ||
67 | + } | ||
68 | + | ||
69 | + setDisabledState(isDisabled: boolean): void { | ||
70 | + this.disabled = isDisabled; | ||
71 | + } | ||
72 | + | ||
73 | + writeValue(datePeriod: FixedWindow): void { | ||
74 | + this.modelValue = datePeriod; | ||
75 | + if (this.modelValue) { | ||
76 | + this.startDate = new Date(this.modelValue.startTimeMs); | ||
77 | + this.endDate = new Date(this.modelValue.endTimeMs); | ||
78 | + } else { | ||
79 | + const date = new Date(); | ||
80 | + this.startDate = new Date( | ||
81 | + date.getFullYear(), | ||
82 | + date.getMonth(), | ||
83 | + date.getDate() - 1, | ||
84 | + date.getHours(), | ||
85 | + date.getMinutes(), | ||
86 | + date.getSeconds(), | ||
87 | + date.getMilliseconds()); | ||
88 | + this.endDate = date; | ||
89 | + this.updateView(); | ||
90 | + } | ||
91 | + this.updateMinMaxDates(); | ||
92 | + } | ||
93 | + | ||
94 | + updateView() { | ||
95 | + let value: FixedWindow = null; | ||
96 | + if (this.startDate && this.endDate) { | ||
97 | + value = new FixedWindow(); | ||
98 | + value.startTimeMs = this.startDate.getTime(); | ||
99 | + value.endTimeMs = this.endDate.getTime(); | ||
100 | + } | ||
101 | + this.modelValue = value; | ||
102 | + if (!this.propagateChange) { | ||
103 | + this.changePending = true; | ||
104 | + } else { | ||
105 | + this.propagateChange(this.modelValue); | ||
106 | + } | ||
107 | + } | ||
108 | + | ||
109 | + updateMinMaxDates() { | ||
110 | + this.maxStartDate = new Date(this.endDate.getTime() - 1000); | ||
111 | + this.minEndDate = new Date(this.startDate.getTime() + 1000); | ||
112 | + this.maxEndDate = new Date(); | ||
113 | + } | ||
114 | + | ||
115 | + onStartDateChange() { | ||
116 | + if (this.startDate) { | ||
117 | + if (this.startDate.getTime() > this.maxStartDate.getTime()) { | ||
118 | + this.startDate = new Date(this.maxStartDate.getTime()); | ||
119 | + } | ||
120 | + this.updateMinMaxDates(); | ||
121 | + } | ||
122 | + this.updateView(); | ||
123 | + } | ||
124 | + | ||
125 | + onEndDateChange() { | ||
126 | + if (this.endDate) { | ||
127 | + if (this.endDate.getTime() < this.minEndDate.getTime()) { | ||
128 | + this.endDate = new Date(this.minEndDate.getTime()); | ||
129 | + } else if (this.endDate.getTime() > this.maxEndDate.getTime()) { | ||
130 | + this.endDate = new Date(this.maxEndDate.getTime()); | ||
131 | + } | ||
132 | + this.updateMinMaxDates(); | ||
133 | + } | ||
134 | + this.updateView(); | ||
135 | + } | ||
136 | + | ||
137 | +} |
1 | +<!-- | ||
2 | + | ||
3 | + Copyright © 2016-2019 The Thingsboard Authors | ||
4 | + | ||
5 | + Licensed under the Apache License, Version 2.0 (the "License"); | ||
6 | + you may not use this file except in compliance with the License. | ||
7 | + You may obtain a copy of the License at | ||
8 | + | ||
9 | + http://www.apache.org/licenses/LICENSE-2.0 | ||
10 | + | ||
11 | + Unless required by applicable law or agreed to in writing, software | ||
12 | + distributed under the License is distributed on an "AS IS" BASIS, | ||
13 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
14 | + See the License for the specific language governing permissions and | ||
15 | + limitations under the License. | ||
16 | + | ||
17 | +--> | ||
18 | +<section fxLayout="row"> | ||
19 | + <section class="interval-section" fxLayout="column" fxFlex [fxShow]="advanced"> | ||
20 | + <label class="tb-small interval-label" translate>{{ predefinedName }}</label> | ||
21 | + <section fxLayout="row" fxLayoutAlign="start start" fxFlex fxLayoutGap="6px"> | ||
22 | + <mat-form-field class="number-input"> | ||
23 | + <mat-label translate>timeinterval.days</mat-label> | ||
24 | + <input matInput type="number" step="1" min="0" [(ngModel)]="days" (ngModelChange)="onTimeInputChange('days')"/> | ||
25 | + </mat-form-field> | ||
26 | + <mat-form-field class="number-input"> | ||
27 | + <mat-label translate>timeinterval.hours</mat-label> | ||
28 | + <input matInput type="number" step="1" [(ngModel)]="hours" (ngModelChange)="onTimeInputChange('hours')"/> | ||
29 | + </mat-form-field> | ||
30 | + <mat-form-field class="number-input"> | ||
31 | + <mat-label translate>timeinterval.minutes</mat-label> | ||
32 | + <input matInput type="number" step="1" [(ngModel)]="mins" (ngModelChange)="onTimeInputChange('mins')"/> | ||
33 | + </mat-form-field> | ||
34 | + <mat-form-field class="number-input"> | ||
35 | + <mat-label translate>timeinterval.seconds</mat-label> | ||
36 | + <input matInput type="number" step="1" [(ngModel)]="secs" (ngModelChange)="onTimeInputChange('secs')"/> | ||
37 | + </mat-form-field> | ||
38 | + </section> | ||
39 | + </section> | ||
40 | + <section class="interval-section" fxLayout="row" fxFlex [fxShow]="!advanced"> | ||
41 | + <mat-form-field fxFlex> | ||
42 | + <mat-label translate>{{ predefinedName }}</mat-label> | ||
43 | + <mat-select matInput [(ngModel)]="intervalMs" (ngModelChange)="onIntervalMsChange()" style="min-width: 150px;"> | ||
44 | + <mat-option *ngFor="let interval of intervals" [value]="interval.value"> | ||
45 | + {{ interval.name | translate:interval.translateParams }} | ||
46 | + </mat-option> | ||
47 | + </mat-select> | ||
48 | + </mat-form-field> | ||
49 | + </section> | ||
50 | + <section fxLayout="column" fxLayoutAlign="center center"> | ||
51 | + <label class="tb-small advanced-label" translate>timeinterval.advanced</label> | ||
52 | + <mat-slide-toggle class="advanced-switch" [(ngModel)]="advanced" (ngModelChange)="onAdvancedChange()"></mat-slide-toggle> | ||
53 | + </section> | ||
54 | +</section> |
1 | +/** | ||
2 | + * Copyright © 2016-2019 The Thingsboard Authors | ||
3 | + * | ||
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | + * you may not use this file except in compliance with the License. | ||
6 | + * You may obtain a copy of the License at | ||
7 | + * | ||
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | + * | ||
10 | + * Unless required by applicable law or agreed to in writing, software | ||
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | + * See the License for the specific language governing permissions and | ||
14 | + * limitations under the License. | ||
15 | + */ | ||
16 | +:host { | ||
17 | + min-width: 355px; | ||
18 | + | ||
19 | + .advanced-switch { | ||
20 | + margin-bottom: 16px; | ||
21 | + } | ||
22 | + | ||
23 | + .advanced-label { | ||
24 | + margin: 5px 0; | ||
25 | + } | ||
26 | + | ||
27 | + .interval-section { | ||
28 | + min-height: 66px; | ||
29 | + .interval-label { | ||
30 | + margin-bottom: 7px; | ||
31 | + margin-top: -1px; | ||
32 | + } | ||
33 | + } | ||
34 | + | ||
35 | +} | ||
36 | + | ||
37 | +:host ::ng-deep { | ||
38 | + .number-input { | ||
39 | + .mat-form-field-infix { | ||
40 | + width: 70px; | ||
41 | + } | ||
42 | + } | ||
43 | +} |
1 | +/// | ||
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | ||
3 | +/// | ||
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | +/// you may not use this file except in compliance with the License. | ||
6 | +/// You may obtain a copy of the License at | ||
7 | +/// | ||
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | +/// | ||
10 | +/// Unless required by applicable law or agreed to in writing, software | ||
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | +/// See the License for the specific language governing permissions and | ||
14 | +/// limitations under the License. | ||
15 | +/// | ||
16 | + | ||
17 | +import { ChangeDetectorRef, Component, forwardRef, Input, OnInit } from '@angular/core'; | ||
18 | +import { | ||
19 | + ControlValueAccessor, | ||
20 | + FormControl, | ||
21 | + NG_VALIDATORS, | ||
22 | + NG_VALUE_ACCESSOR, | ||
23 | + Validator | ||
24 | +} from '@angular/forms'; | ||
25 | +import { Timewindow } from '@shared/models/time/time.models'; | ||
26 | +import { TimeInterval, TimeService } from '@core/services/time.service'; | ||
27 | + | ||
28 | +@Component({ | ||
29 | + selector: 'tb-timeinterval', | ||
30 | + templateUrl: './timeinterval.component.html', | ||
31 | + styleUrls: ['./timeinterval.component.scss'], | ||
32 | + providers: [ | ||
33 | + { | ||
34 | + provide: NG_VALUE_ACCESSOR, | ||
35 | + useExisting: forwardRef(() => TimeintervalComponent), | ||
36 | + multi: true | ||
37 | + } | ||
38 | + ] | ||
39 | +}) | ||
40 | +export class TimeintervalComponent implements OnInit, ControlValueAccessor { | ||
41 | + | ||
42 | + minValue: number; | ||
43 | + maxValue: number; | ||
44 | + | ||
45 | + @Input() | ||
46 | + set min(min: number) { | ||
47 | + if (typeof min !== 'undefined' && min !== this.minValue) { | ||
48 | + this.minValue = min; | ||
49 | + this.updateView(); | ||
50 | + } | ||
51 | + } | ||
52 | + | ||
53 | + @Input() | ||
54 | + set max(max: number) { | ||
55 | + if (typeof max !== 'undefined' && max !== this.maxValue) { | ||
56 | + this.maxValue = max; | ||
57 | + this.updateView(); | ||
58 | + } | ||
59 | + } | ||
60 | + | ||
61 | + @Input() predefinedName: string; | ||
62 | + @Input() disabled: boolean; | ||
63 | + | ||
64 | + days = 0; | ||
65 | + hours = 0; | ||
66 | + mins = 1; | ||
67 | + secs = 0; | ||
68 | + | ||
69 | + intervalMs = 0; | ||
70 | + modelValue: number; | ||
71 | + | ||
72 | + advanced = false; | ||
73 | + rendered = false; | ||
74 | + | ||
75 | + intervals: Array<TimeInterval>; | ||
76 | + | ||
77 | + private propagateChange = (_: any) => {}; | ||
78 | + | ||
79 | + constructor(private timeService: TimeService) { | ||
80 | + } | ||
81 | + | ||
82 | + ngOnInit(): void { | ||
83 | + this.boundInterval(); | ||
84 | + } | ||
85 | + | ||
86 | + registerOnChange(fn: any): void { | ||
87 | + this.propagateChange = fn; | ||
88 | + } | ||
89 | + | ||
90 | + registerOnTouched(fn: any): void { | ||
91 | + } | ||
92 | + | ||
93 | + setDisabledState(isDisabled: boolean): void { | ||
94 | + this.disabled = isDisabled; | ||
95 | + } | ||
96 | + | ||
97 | + writeValue(intervalMs: number): void { | ||
98 | + this.modelValue = intervalMs; | ||
99 | + this.rendered = true; | ||
100 | + if (typeof this.modelValue !== 'undefined') { | ||
101 | + const min = this.timeService.boundMinInterval(this.minValue); | ||
102 | + const max = this.timeService.boundMaxInterval(this.maxValue); | ||
103 | + if (this.modelValue >= min && this.modelValue <= max) { | ||
104 | + this.advanced = !this.timeService.matchesExistingInterval(this.minValue, this.maxValue, this.modelValue); | ||
105 | + this.setIntervalMs(this.modelValue); | ||
106 | + } else { | ||
107 | + this.boundInterval(); | ||
108 | + } | ||
109 | + } | ||
110 | + } | ||
111 | + | ||
112 | + setIntervalMs(intervalMs: number) { | ||
113 | + if (!this.advanced) { | ||
114 | + this.intervalMs = intervalMs; | ||
115 | + } | ||
116 | + const intervalSeconds = Math.floor(intervalMs / 1000); | ||
117 | + this.days = Math.floor(intervalSeconds / 86400); | ||
118 | + this.hours = Math.floor((intervalSeconds % 86400) / 3600); | ||
119 | + this.mins = Math.floor(((intervalSeconds % 86400) % 3600) / 60); | ||
120 | + this.secs = intervalSeconds % 60; | ||
121 | + } | ||
122 | + | ||
123 | + boundInterval() { | ||
124 | + const min = this.timeService.boundMinInterval(this.minValue); | ||
125 | + const max = this.timeService.boundMaxInterval(this.maxValue); | ||
126 | + this.intervals = this.timeService.getIntervals(this.minValue, this.maxValue); | ||
127 | + if (this.rendered) { | ||
128 | + let newIntervalMs = this.modelValue; | ||
129 | + if (newIntervalMs < min) { | ||
130 | + newIntervalMs = min; | ||
131 | + } else if (newIntervalMs > max) { | ||
132 | + newIntervalMs = max; | ||
133 | + } | ||
134 | + if (!this.advanced) { | ||
135 | + newIntervalMs = this.timeService.boundToPredefinedInterval(min, max, newIntervalMs); | ||
136 | + } | ||
137 | + if (newIntervalMs !== this.modelValue) { | ||
138 | + this.setIntervalMs(newIntervalMs); | ||
139 | + this.updateView(); | ||
140 | + } | ||
141 | + } | ||
142 | + } | ||
143 | + | ||
144 | + updateView() { | ||
145 | + if (!this.rendered) { | ||
146 | + return; | ||
147 | + } | ||
148 | + let value = null; | ||
149 | + let intervalMs; | ||
150 | + if (!this.advanced) { | ||
151 | + intervalMs = this.intervalMs; | ||
152 | + if (!intervalMs || isNaN(intervalMs)) { | ||
153 | + intervalMs = this.calculateIntervalMs(); | ||
154 | + } | ||
155 | + } else { | ||
156 | + intervalMs = this.calculateIntervalMs(); | ||
157 | + } | ||
158 | + if (!isNaN(intervalMs) && intervalMs > 0) { | ||
159 | + value = intervalMs; | ||
160 | + } | ||
161 | + this.modelValue = value; | ||
162 | + this.propagateChange(this.modelValue); | ||
163 | + this.boundInterval(); | ||
164 | + } | ||
165 | + | ||
166 | + calculateIntervalMs(): number { | ||
167 | + return (this.days * 86400 + | ||
168 | + this.hours * 3600 + | ||
169 | + this.mins * 60 + | ||
170 | + this.secs) * 1000; | ||
171 | + } | ||
172 | + | ||
173 | + onIntervalMsChange() { | ||
174 | + this.updateView(); | ||
175 | + } | ||
176 | + | ||
177 | + onAdvancedChange() { | ||
178 | + if (!this.advanced) { | ||
179 | + this.intervalMs = this.calculateIntervalMs(); | ||
180 | + } else { | ||
181 | + let intervalMs = this.intervalMs; | ||
182 | + if (!intervalMs || isNaN(intervalMs)) { | ||
183 | + intervalMs = this.calculateIntervalMs(); | ||
184 | + } | ||
185 | + this.setIntervalMs(intervalMs); | ||
186 | + } | ||
187 | + this.updateView(); | ||
188 | + } | ||
189 | + | ||
190 | + onTimeInputChange(type: string) { | ||
191 | + switch (type) { | ||
192 | + case 'secs': | ||
193 | + setTimeout(() => this.onSecsChange(), 0); | ||
194 | + break; | ||
195 | + case 'mins': | ||
196 | + setTimeout(() => this.onMinsChange(), 0); | ||
197 | + break; | ||
198 | + case 'hours': | ||
199 | + setTimeout(() => this.onHoursChange(), 0); | ||
200 | + break; | ||
201 | + case 'days': | ||
202 | + setTimeout(() => this.onDaysChange(), 0); | ||
203 | + break; | ||
204 | + } | ||
205 | + } | ||
206 | + | ||
207 | + onSecsChange() { | ||
208 | + if (typeof this.secs === 'undefined') { | ||
209 | + return; | ||
210 | + } | ||
211 | + if (this.secs < 0) { | ||
212 | + if ((this.days + this.hours + this.mins) > 0) { | ||
213 | + this.secs = this.secs + 60; | ||
214 | + this.mins--; | ||
215 | + this.onMinsChange(); | ||
216 | + } else { | ||
217 | + this.secs = 0; | ||
218 | + } | ||
219 | + } else if (this.secs >= 60) { | ||
220 | + this.secs = this.secs - 60; | ||
221 | + this.mins++; | ||
222 | + this.onMinsChange(); | ||
223 | + } | ||
224 | + this.updateView(); | ||
225 | + } | ||
226 | + | ||
227 | + onMinsChange() { | ||
228 | + if (typeof this.mins === 'undefined') { | ||
229 | + return; | ||
230 | + } | ||
231 | + if (this.mins < 0) { | ||
232 | + if ((this.days + this.hours) > 0) { | ||
233 | + this.mins = this.mins + 60; | ||
234 | + this.hours--; | ||
235 | + this.onHoursChange(); | ||
236 | + } else { | ||
237 | + this.mins = 0; | ||
238 | + } | ||
239 | + } else if (this.mins >= 60) { | ||
240 | + this.mins = this.mins - 60; | ||
241 | + this.hours++; | ||
242 | + this.onHoursChange(); | ||
243 | + } | ||
244 | + this.updateView(); | ||
245 | + } | ||
246 | + | ||
247 | + onHoursChange() { | ||
248 | + if (typeof this.hours === 'undefined') { | ||
249 | + return; | ||
250 | + } | ||
251 | + if (this.hours < 0) { | ||
252 | + if (this.days > 0) { | ||
253 | + this.hours = this.hours + 24; | ||
254 | + this.days--; | ||
255 | + this.onDaysChange(); | ||
256 | + } else { | ||
257 | + this.hours = 0; | ||
258 | + } | ||
259 | + } else if (this.hours >= 24) { | ||
260 | + this.hours = this.hours - 24; | ||
261 | + this.days++; | ||
262 | + this.onDaysChange(); | ||
263 | + } | ||
264 | + this.updateView(); | ||
265 | + } | ||
266 | + | ||
267 | + onDaysChange() { | ||
268 | + if (typeof this.days === 'undefined') { | ||
269 | + return; | ||
270 | + } | ||
271 | + if (this.days < 0) { | ||
272 | + this.days = 0; | ||
273 | + } | ||
274 | + this.updateView(); | ||
275 | + } | ||
276 | + | ||
277 | +} |
1 | +<!-- | ||
2 | + | ||
3 | + Copyright © 2016-2019 The Thingsboard Authors | ||
4 | + | ||
5 | + Licensed under the Apache License, Version 2.0 (the "License"); | ||
6 | + you may not use this file except in compliance with the License. | ||
7 | + You may obtain a copy of the License at | ||
8 | + | ||
9 | + http://www.apache.org/licenses/LICENSE-2.0 | ||
10 | + | ||
11 | + Unless required by applicable law or agreed to in writing, software | ||
12 | + distributed under the License is distributed on an "AS IS" BASIS, | ||
13 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
14 | + See the License for the specific language governing permissions and | ||
15 | + limitations under the License. | ||
16 | + | ||
17 | +--> | ||
18 | +<form [formGroup]="timewindowForm" (ngSubmit)="update()"> | ||
19 | + <fieldset [disabled]="(isLoading$ | async)"> | ||
20 | + <div class="mat-content" style="height: 100%;" fxFlex fxLayout="column"> | ||
21 | + <section fxLayout="column"> | ||
22 | + <mat-tab-group dynamicHeight [ngClass]="{'tb-headless': historyOnly}" | ||
23 | + (selectedIndexChange)="timewindowForm.markAsDirty()" [(selectedIndex)]="timewindow.selectedTab"> | ||
24 | + <mat-tab label="{{ 'timewindow.realtime' | translate }}"> | ||
25 | + <div formGroupName="realtime" class="mat-content mat-padding" fxLayout="column"> | ||
26 | + <tb-timeinterval | ||
27 | + formControlName="timewindowMs" | ||
28 | + predefinedName="timewindow.last" | ||
29 | + [required]="timewindow.selectedTab === timewindowTypes.REALTIME" | ||
30 | + style="padding-top: 8px;"></tb-timeinterval> | ||
31 | + </div> | ||
32 | + </mat-tab> | ||
33 | + <mat-tab label="{{ 'timewindow.history' | translate }}"> | ||
34 | + <div formGroupName="history" class="mat-content mat-padding" style="padding-top: 8px;"> | ||
35 | + <mat-radio-group formControlName="historyType"> | ||
36 | + <mat-radio-button [value]="historyTypes.LAST_INTERVAL" color="primary"> | ||
37 | + <section fxLayout="column"> | ||
38 | + <tb-timeinterval | ||
39 | + formControlName="timewindowMs" | ||
40 | + predefinedName="timewindow.last" | ||
41 | + [fxShow]="timewindowForm.get('history').get('historyType').value === historyTypes.LAST_INTERVAL" | ||
42 | + [required]="timewindow.selectedTab === timewindowTypes.HISTORY && | ||
43 | + timewindowForm.get('history').get('historyType').value === historyTypes.LAST_INTERVAL" | ||
44 | + style="padding-top: 8px;"></tb-timeinterval> | ||
45 | + </section> | ||
46 | + </mat-radio-button> | ||
47 | + <mat-radio-button [value]="historyTypes.FIXED" color="primary"> | ||
48 | + <section fxLayout="column"> | ||
49 | + <span translate>timewindow.time-period</span> | ||
50 | + <tb-datetime-period | ||
51 | + formControlName="fixedTimewindow" | ||
52 | + [fxShow]="timewindowForm.get('history').get('historyType').value === historyTypes.FIXED" | ||
53 | + [required]="timewindow.selectedTab === timewindowTypes.HISTORY && | ||
54 | + timewindowForm.get('history').get('historyType').value === historyTypes.FIXED" | ||
55 | + style="padding-top: 8px;"></tb-datetime-period> | ||
56 | + </section> | ||
57 | + </mat-radio-button> | ||
58 | + </mat-radio-group> | ||
59 | + </div> | ||
60 | + </mat-tab> | ||
61 | + </mat-tab-group> | ||
62 | + <div *ngIf="aggregation" formGroupName="aggregation" class="mat-content mat-padding" fxLayout="column"> | ||
63 | + <mat-form-field> | ||
64 | + <mat-label translate>aggregation.function</mat-label> | ||
65 | + <mat-select matInput formControlName="type" style="min-width: 150px;"> | ||
66 | + <mat-option *ngFor="let aggregation of aggregations" [value]="aggregation"> | ||
67 | + {{ aggregationTypesTranslations.get(aggregation) | translate }} | ||
68 | + </mat-option> | ||
69 | + </mat-select> | ||
70 | + </mat-form-field> | ||
71 | + <div *ngIf="timewindowForm.get('aggregation').get('type').value === aggregationTypes.NONE" | ||
72 | + class="limit-slider-container" | ||
73 | + fxLayout="row" fxLayoutAlign="start center"> | ||
74 | + <span translate>aggregation.limit</span> | ||
75 | + <mat-slider fxFlex formControlName="limit" | ||
76 | + thumbLabel | ||
77 | + [value]="timewindowForm.get('aggregation').get('limit').value" | ||
78 | + min="{{minDatapointsLimit()}}" | ||
79 | + max="{{maxDatapointsLimit()}}"> | ||
80 | + </mat-slider> | ||
81 | + <mat-form-field style="max-width: 80px;"> | ||
82 | + <input matInput formControlName="limit" type="number" step="1" | ||
83 | + [value]="timewindowForm.get('aggregation').get('limit').value" | ||
84 | + min="{{minDatapointsLimit()}}" | ||
85 | + max="{{maxDatapointsLimit()}}"/> | ||
86 | + </mat-form-field> | ||
87 | + </div> | ||
88 | + </div> | ||
89 | + <div formGroupName="realtime" | ||
90 | + *ngIf="aggregation && timewindowForm.get('aggregation').get('type').value !== aggregationTypes.NONE && | ||
91 | + timewindow.selectedTab === timewindowTypes.REALTIME" class="mat-content mat-padding" fxLayout="column"> | ||
92 | + <tb-timeinterval | ||
93 | + formControlName="interval" | ||
94 | + [min]="minRealtimeAggInterval()" [max]="maxRealtimeAggInterval()" | ||
95 | + predefinedName="aggregation.group-interval"> | ||
96 | + </tb-timeinterval> | ||
97 | + </div> | ||
98 | + <div formGroupName="history" | ||
99 | + *ngIf="aggregation && timewindowForm.get('aggregation').get('type').value !== aggregationTypes.NONE && | ||
100 | + timewindow.selectedTab === timewindowTypes.HISTORY" class="mat-content mat-padding" fxLayout="column"> | ||
101 | + <tb-timeinterval | ||
102 | + formControlName="interval" | ||
103 | + [min]="minHistoryAggInterval()" [max]="maxHistoryAggInterval()" | ||
104 | + predefinedName="aggregation.group-interval"> | ||
105 | + </tb-timeinterval> | ||
106 | + </div> | ||
107 | + </section> | ||
108 | + <span fxFlex></span> | ||
109 | + <div fxLayout="row" class="tb-panel-actions"> | ||
110 | + <span fxFlex></span> | ||
111 | + <button type="submit" | ||
112 | + mat-raised-button | ||
113 | + color="primary" | ||
114 | + [disabled]="(isLoading$ | async) || timewindowForm.invalid || !timewindowForm.dirty"> | ||
115 | + {{ 'action.update' | translate }} | ||
116 | + </button> | ||
117 | + <button type="button" | ||
118 | + mat-button | ||
119 | + [disabled]="(isLoading$ | async)" | ||
120 | + (click)="cancel()" | ||
121 | + style="margin-right: 20px;"> | ||
122 | + {{ 'action.cancel' | translate }} | ||
123 | + </button> | ||
124 | + </div> | ||
125 | + </div> | ||
126 | + </fieldset> | ||
127 | +</form> |
1 | +/** | ||
2 | + * Copyright © 2016-2019 The Thingsboard Authors | ||
3 | + * | ||
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | + * you may not use this file except in compliance with the License. | ||
6 | + * You may obtain a copy of the License at | ||
7 | + * | ||
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | + * | ||
10 | + * Unless required by applicable law or agreed to in writing, software | ||
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | + * See the License for the specific language governing permissions and | ||
14 | + * limitations under the License. | ||
15 | + */ | ||
16 | +:host { | ||
17 | + width: 100%; | ||
18 | + height: 100%; | ||
19 | + form, | ||
20 | + fieldset { | ||
21 | + height: 100%; | ||
22 | + } | ||
23 | + | ||
24 | + .mat-content { | ||
25 | + overflow: hidden; | ||
26 | + background-color: #fff; | ||
27 | + } | ||
28 | + | ||
29 | + .mat-padding { | ||
30 | + padding: 0 16px; | ||
31 | + } | ||
32 | + | ||
33 | + .limit-slider-container { | ||
34 | + >:first-child { | ||
35 | + margin-right: 16px; | ||
36 | + } | ||
37 | + >:last-child { | ||
38 | + margin-left: 16px; | ||
39 | + } | ||
40 | + >:first-child, >:last-child { | ||
41 | + min-width: 25px; | ||
42 | + max-width: 42px; | ||
43 | + } | ||
44 | + mat-form-field input[type=number] { | ||
45 | + text-align: center; | ||
46 | + } | ||
47 | + } | ||
48 | + | ||
49 | +} | ||
50 | + | ||
51 | +:host ::ng-deep { | ||
52 | + mat-radio-button { | ||
53 | + display: block; | ||
54 | + margin-bottom: 16px; | ||
55 | + .mat-radio-label { | ||
56 | + width: 100%; | ||
57 | + align-items: start; | ||
58 | + .mat-radio-label-content { | ||
59 | + width: 100%; | ||
60 | + } | ||
61 | + } | ||
62 | + } | ||
63 | +} |
1 | +/// | ||
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | ||
3 | +/// | ||
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | +/// you may not use this file except in compliance with the License. | ||
6 | +/// You may obtain a copy of the License at | ||
7 | +/// | ||
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | +/// | ||
10 | +/// Unless required by applicable law or agreed to in writing, software | ||
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | +/// See the License for the specific language governing permissions and | ||
14 | +/// limitations under the License. | ||
15 | +/// | ||
16 | + | ||
17 | +import { | ||
18 | + Component, | ||
19 | + Inject, | ||
20 | + InjectionToken, | ||
21 | + OnInit, | ||
22 | + ViewChild, | ||
23 | + ViewContainerRef | ||
24 | +} from '@angular/core'; | ||
25 | +import { TranslateService } from '@ngx-translate/core'; | ||
26 | +import { MillisecondsToTimeStringPipe } from '@shared/pipe/milliseconds-to-time-string.pipe'; | ||
27 | +import { | ||
28 | + Aggregation, | ||
29 | + aggregationTranslations, | ||
30 | + AggregationType, | ||
31 | + HistoryWindow, | ||
32 | + HistoryWindowType, | ||
33 | + IntervalWindow, | ||
34 | + Timewindow, | ||
35 | + TimewindowType | ||
36 | +} from '@shared/models/time/time.models'; | ||
37 | +import { DatePipe } from '@angular/common'; | ||
38 | +import { Overlay, OverlayRef } from '@angular/cdk/overlay'; | ||
39 | +import { PageComponent } from '@shared/components/page.component'; | ||
40 | +import { Store } from '@ngrx/store'; | ||
41 | +import { AppState } from '@core/core.state'; | ||
42 | +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; | ||
43 | +import { TimeService } from '@core/services/time.service'; | ||
44 | + | ||
45 | +export const TIMEWINDOW_PANEL_DATA = new InjectionToken<any>('TimewindowPanelData'); | ||
46 | + | ||
47 | +export interface TimewindowPanelData { | ||
48 | + historyOnly: boolean; | ||
49 | + timewindow: Timewindow; | ||
50 | + aggregation: boolean; | ||
51 | +} | ||
52 | + | ||
53 | +@Component({ | ||
54 | + selector: 'tb-timewindow-panel', | ||
55 | + templateUrl: './timewindow-panel.component.html', | ||
56 | + styleUrls: ['./timewindow-panel.component.scss'] | ||
57 | +}) | ||
58 | +export class TimewindowPanelComponent extends PageComponent implements OnInit { | ||
59 | + | ||
60 | + historyOnly = false; | ||
61 | + | ||
62 | + aggregation = false; | ||
63 | + | ||
64 | + timewindow: Timewindow; | ||
65 | + | ||
66 | + result: Timewindow; | ||
67 | + | ||
68 | + timewindowForm: FormGroup; | ||
69 | + | ||
70 | + historyTypes = HistoryWindowType; | ||
71 | + | ||
72 | + timewindowTypes = TimewindowType; | ||
73 | + | ||
74 | + aggregationTypes = AggregationType; | ||
75 | + | ||
76 | + aggregations = Object.keys(AggregationType); | ||
77 | + | ||
78 | + aggregationTypesTranslations = aggregationTranslations; | ||
79 | + | ||
80 | + constructor(@Inject(TIMEWINDOW_PANEL_DATA) public data: TimewindowPanelData, | ||
81 | + public overlayRef: OverlayRef, | ||
82 | + protected store: Store<AppState>, | ||
83 | + public fb: FormBuilder, | ||
84 | + private timeService: TimeService, | ||
85 | + private translate: TranslateService, | ||
86 | + private millisecondsToTimeStringPipe: MillisecondsToTimeStringPipe, | ||
87 | + private datePipe: DatePipe, | ||
88 | + private overlay: Overlay, | ||
89 | + public viewContainerRef: ViewContainerRef) { | ||
90 | + super(store); | ||
91 | + this.historyOnly = data.historyOnly; | ||
92 | + this.timewindow = data.timewindow; | ||
93 | + this.aggregation = data.aggregation; | ||
94 | + } | ||
95 | + | ||
96 | + ngOnInit(): void { | ||
97 | + this.timewindowForm = this.fb.group({ | ||
98 | + realtime: this.fb.group( | ||
99 | + { | ||
100 | + timewindowMs: [ | ||
101 | + this.timewindow.realtime && typeof this.timewindow.realtime.timewindowMs !== 'undefined' | ||
102 | + ? this.timewindow.realtime.timewindowMs : null | ||
103 | + ], | ||
104 | + interval: [ | ||
105 | + this.timewindow.realtime && typeof this.timewindow.realtime.interval !== 'undefined' | ||
106 | + ? this.timewindow.realtime.interval : null | ||
107 | + ] | ||
108 | + } | ||
109 | + ), | ||
110 | + history: this.fb.group( | ||
111 | + { | ||
112 | + historyType: [ | ||
113 | + this.timewindow.history && typeof this.timewindow.history.historyType !== 'undefined' | ||
114 | + ? this.timewindow.history.historyType : HistoryWindowType.LAST_INTERVAL | ||
115 | + ], | ||
116 | + timewindowMs: [ | ||
117 | + this.timewindow.history && typeof this.timewindow.history.timewindowMs !== 'undefined' | ||
118 | + ? this.timewindow.history.timewindowMs : null | ||
119 | + ], | ||
120 | + interval: [ | ||
121 | + this.timewindow.history && typeof this.timewindow.history.interval !== 'undefined' | ||
122 | + ? this.timewindow.history.interval : null | ||
123 | + ], | ||
124 | + fixedTimewindow: [ | ||
125 | + this.timewindow.history && typeof this.timewindow.history.fixedTimewindow !== 'undefined' | ||
126 | + ? this.timewindow.history.fixedTimewindow : null | ||
127 | + ] | ||
128 | + } | ||
129 | + ), | ||
130 | + aggregation: this.fb.group( | ||
131 | + { | ||
132 | + type: [ | ||
133 | + this.timewindow.aggregation && typeof this.timewindow.aggregation.type !== 'undefined' | ||
134 | + ? this.timewindow.aggregation.type : null | ||
135 | + ], | ||
136 | + limit: [ | ||
137 | + this.timewindow.aggregation && typeof this.timewindow.aggregation.limit !== 'undefined' | ||
138 | + ? this.timewindow.aggregation.limit : null, | ||
139 | + [Validators.min(this.minDatapointsLimit()), Validators.max(this.maxDatapointsLimit())] | ||
140 | + ] | ||
141 | + } | ||
142 | + ) | ||
143 | + }); | ||
144 | + } | ||
145 | + | ||
146 | + update() { | ||
147 | + const timewindowFormValue = this.timewindowForm.value; | ||
148 | + this.timewindow.realtime = new IntervalWindow(); | ||
149 | + this.timewindow.realtime.timewindowMs = timewindowFormValue.realtime.timewindowMs; | ||
150 | + this.timewindow.realtime.interval = timewindowFormValue.realtime.interval; | ||
151 | + this.timewindow.history = new HistoryWindow(); | ||
152 | + this.timewindow.history.historyType = timewindowFormValue.history.historyType; | ||
153 | + this.timewindow.history.timewindowMs = timewindowFormValue.history.timewindowMs; | ||
154 | + this.timewindow.history.interval = timewindowFormValue.history.interval; | ||
155 | + this.timewindow.history.fixedTimewindow = timewindowFormValue.history.fixedTimewindow; | ||
156 | + if (this.aggregation) { | ||
157 | + this.timewindow.aggregation = new Aggregation(); | ||
158 | + this.timewindow.aggregation.type = timewindowFormValue.aggregation.type; | ||
159 | + this.timewindow.aggregation.limit = timewindowFormValue.aggregation.limit; | ||
160 | + } | ||
161 | + this.result = this.timewindow; | ||
162 | + this.overlayRef.dispose(); | ||
163 | + } | ||
164 | + | ||
165 | + cancel() { | ||
166 | + this.overlayRef.dispose(); | ||
167 | + } | ||
168 | + | ||
169 | + minDatapointsLimit() { | ||
170 | + return this.timeService.getMinDatapointsLimit(); | ||
171 | + } | ||
172 | + | ||
173 | + maxDatapointsLimit() { | ||
174 | + return this.timeService.getMaxDatapointsLimit(); | ||
175 | + } | ||
176 | + | ||
177 | + minRealtimeAggInterval() { | ||
178 | + return this.timeService.minIntervalLimit(this.timewindowForm.get('realtime').get('timewindowMs').value); | ||
179 | + } | ||
180 | + | ||
181 | + maxRealtimeAggInterval() { | ||
182 | + return this.timeService.maxIntervalLimit(this.timewindowForm.get('realtime').get('timewindowMs').value); | ||
183 | + } | ||
184 | + | ||
185 | + minHistoryAggInterval() { | ||
186 | + return this.timeService.minIntervalLimit(this.currentHistoryTimewindow()); | ||
187 | + } | ||
188 | + | ||
189 | + maxHistoryAggInterval() { | ||
190 | + return this.timeService.maxIntervalLimit(this.currentHistoryTimewindow()); | ||
191 | + } | ||
192 | + | ||
193 | + currentHistoryTimewindow() { | ||
194 | + const timewindowFormValue = this.timewindowForm.value; | ||
195 | + if (timewindowFormValue.history.historyType === HistoryWindowType.LAST_INTERVAL) { | ||
196 | + return timewindowFormValue.history.timewindowMs; | ||
197 | + } else { | ||
198 | + return timewindowFormValue.history.fixedTimewindow.endTimeMs - | ||
199 | + timewindowFormValue.history.fixedTimewindow.startTimeMs; | ||
200 | + } | ||
201 | + } | ||
202 | + | ||
203 | +} |
1 | +<!-- | ||
2 | + | ||
3 | + Copyright © 2016-2019 The Thingsboard Authors | ||
4 | + | ||
5 | + Licensed under the Apache License, Version 2.0 (the "License"); | ||
6 | + you may not use this file except in compliance with the License. | ||
7 | + You may obtain a copy of the License at | ||
8 | + | ||
9 | + http://www.apache.org/licenses/LICENSE-2.0 | ||
10 | + | ||
11 | + Unless required by applicable law or agreed to in writing, software | ||
12 | + distributed under the License is distributed on an "AS IS" BASIS, | ||
13 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
14 | + See the License for the specific language governing permissions and | ||
15 | + limitations under the License. | ||
16 | + | ||
17 | +--> | ||
18 | +<button *ngIf="asButton" cdkOverlayOrigin #timewindowPanelOrigin="cdkOverlayOrigin" [disabled]="disabled" | ||
19 | + mat-raised-button color="primary" (click)="openEditMode($event)"> | ||
20 | + <mat-icon class="material-icons">query_builder</mat-icon> | ||
21 | + <span>{{innerValue.displayValue}}</span> | ||
22 | +</button> | ||
23 | +<section *ngIf="!asButton" cdkOverlayOrigin #timewindowPanelOrigin="cdkOverlayOrigin" | ||
24 | + class="tb-timewindow" fxLayout="row" fxLayoutAlign="start center"> | ||
25 | + <button *ngIf="direction === 'left'" [disabled]="disabled" mat-button mat-icon-button class="tb-mat-32" | ||
26 | + (click)="openEditMode($event)" | ||
27 | + matTooltip="{{ 'timewindow.edit' | translate }}" | ||
28 | + [matTooltipPosition]="tooltipPosition"> | ||
29 | + <mat-icon class="material-icons">query_builder</mat-icon> | ||
30 | + </button> | ||
31 | + <span [fxHide]="hideLabel()" | ||
32 | + (click)="openEditMode($event)" | ||
33 | + matTooltip="{{ 'timewindow.edit' | translate }}" | ||
34 | + [matTooltipPosition]="tooltipPosition"> | ||
35 | + {{innerValue.displayValue}} | ||
36 | + </span> | ||
37 | + <button *ngIf="direction === 'right'" [disabled]="disabled" mat-button mat-icon-button class="tb-mat-32" | ||
38 | + (click)="openEditMode($event)" | ||
39 | + matTooltip="{{ 'timewindow.edit' | translate }}" | ||
40 | + [matTooltipPosition]="tooltipPosition"> | ||
41 | + <mat-icon class="material-icons">query_builder</mat-icon> | ||
42 | + </button> | ||
43 | +</section> |
1 | +/** | ||
2 | + * Copyright © 2016-2019 The Thingsboard Authors | ||
3 | + * | ||
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | + * you may not use this file except in compliance with the License. | ||
6 | + * You may obtain a copy of the License at | ||
7 | + * | ||
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | + * | ||
10 | + * Unless required by applicable law or agreed to in writing, software | ||
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | + * See the License for the specific language governing permissions and | ||
14 | + * limitations under the License. | ||
15 | + */ | ||
16 | +:host { | ||
17 | + section.tb-timewindow { | ||
18 | + span { | ||
19 | + overflow: hidden; | ||
20 | + text-overflow: ellipsis; | ||
21 | + white-space: nowrap; | ||
22 | + pointer-events: all; | ||
23 | + cursor: pointer; | ||
24 | + } | ||
25 | + } | ||
26 | +} |
1 | +/// | ||
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | ||
3 | +/// | ||
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | +/// you may not use this file except in compliance with the License. | ||
6 | +/// You may obtain a copy of the License at | ||
7 | +/// | ||
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | +/// | ||
10 | +/// Unless required by applicable law or agreed to in writing, software | ||
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | +/// See the License for the specific language governing permissions and | ||
14 | +/// limitations under the License. | ||
15 | +/// | ||
16 | + | ||
17 | +import { | ||
18 | + Component, | ||
19 | + forwardRef, Inject, | ||
20 | + Input, | ||
21 | + OnDestroy, | ||
22 | + OnInit, | ||
23 | + ViewChild, | ||
24 | + ViewContainerRef | ||
25 | +} from '@angular/core'; | ||
26 | +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; | ||
27 | +import { TranslateService } from '@ngx-translate/core'; | ||
28 | +import { MillisecondsToTimeStringPipe } from '@shared/pipe/milliseconds-to-time-string.pipe'; | ||
29 | +import { | ||
30 | + HistoryWindowType, | ||
31 | + Timewindow, | ||
32 | + TimewindowType | ||
33 | +} from '@shared/models/time/time.models'; | ||
34 | +import { DatePipe } from '@angular/common'; | ||
35 | +import { | ||
36 | + Overlay, | ||
37 | + CdkOverlayOrigin, | ||
38 | + OverlayConfig, | ||
39 | + OverlayPositionBuilder, ConnectedPosition, PositionStrategy, OverlayRef | ||
40 | +} from '@angular/cdk/overlay'; | ||
41 | +import { | ||
42 | + TIMEWINDOW_PANEL_DATA, | ||
43 | + TimewindowPanelComponent, | ||
44 | + TimewindowPanelData | ||
45 | +} from '@shared/components/time/timewindow-panel.component'; | ||
46 | +import { ComponentPortal, PortalInjector } from '@angular/cdk/portal'; | ||
47 | +import { MediaBreakpoints } from '@shared/models/constants'; | ||
48 | +import { BreakpointObserver } from '@angular/cdk/layout'; | ||
49 | +import { DOCUMENT } from '@angular/common'; | ||
50 | +import { WINDOW } from '@core/services/window.service'; | ||
51 | +import { TimeService } from '@core/services/time.service'; | ||
52 | +import { TooltipPosition } from '@angular/material/typings/tooltip'; | ||
53 | + | ||
54 | +@Component({ | ||
55 | + selector: 'tb-timewindow', | ||
56 | + templateUrl: './timewindow.component.html', | ||
57 | + styleUrls: ['./timewindow.component.scss'], | ||
58 | + providers: [ | ||
59 | + { | ||
60 | + provide: NG_VALUE_ACCESSOR, | ||
61 | + useExisting: forwardRef(() => TimewindowComponent), | ||
62 | + multi: true | ||
63 | + } | ||
64 | + ] | ||
65 | +}) | ||
66 | +export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAccessor { | ||
67 | + | ||
68 | + historyOnlyValue = false; | ||
69 | + | ||
70 | + @Input() | ||
71 | + set historyOnly(val) { | ||
72 | + this.historyOnlyValue = true; | ||
73 | + } | ||
74 | + | ||
75 | + get historyOnly() { | ||
76 | + return this.historyOnlyValue; | ||
77 | + } | ||
78 | + | ||
79 | + aggregationValue = false; | ||
80 | + | ||
81 | + @Input() | ||
82 | + set aggregation(val) { | ||
83 | + this.aggregationValue = true; | ||
84 | + } | ||
85 | + | ||
86 | + get aggregation() { | ||
87 | + return this.aggregationValue; | ||
88 | + } | ||
89 | + | ||
90 | + isToolbarValue = false; | ||
91 | + | ||
92 | + @Input() | ||
93 | + set isToolbar(val) { | ||
94 | + this.isToolbarValue = true; | ||
95 | + } | ||
96 | + | ||
97 | + get isToolbar() { | ||
98 | + return this.isToolbarValue; | ||
99 | + } | ||
100 | + | ||
101 | + asButtonValue = false; | ||
102 | + | ||
103 | + @Input() | ||
104 | + set asButton(val) { | ||
105 | + this.asButtonValue = true; | ||
106 | + } | ||
107 | + | ||
108 | + get asButton() { | ||
109 | + return this.asButtonValue; | ||
110 | + } | ||
111 | + | ||
112 | + @Input() | ||
113 | + direction: 'left' | 'right' = 'left'; | ||
114 | + | ||
115 | + @Input() | ||
116 | + tooltipPosition: TooltipPosition = 'above'; | ||
117 | + | ||
118 | + @Input() disabled: boolean; | ||
119 | + | ||
120 | + @ViewChild('timewindowPanelOrigin', {static: false}) timewindowPanelOrigin: CdkOverlayOrigin; | ||
121 | + | ||
122 | + innerValue: Timewindow; | ||
123 | + | ||
124 | + private propagateChange = (_: any) => {}; | ||
125 | + | ||
126 | + constructor(private translate: TranslateService, | ||
127 | + private timeService: TimeService, | ||
128 | + private millisecondsToTimeStringPipe: MillisecondsToTimeStringPipe, | ||
129 | + private datePipe: DatePipe, | ||
130 | + private overlay: Overlay, | ||
131 | + public viewContainerRef: ViewContainerRef, | ||
132 | + public breakpointObserver: BreakpointObserver, | ||
133 | + @Inject(DOCUMENT) private document: Document, | ||
134 | + @Inject(WINDOW) private window: Window) { | ||
135 | + } | ||
136 | + | ||
137 | + ngOnInit(): void { | ||
138 | + } | ||
139 | + | ||
140 | + ngOnDestroy(): void { | ||
141 | + } | ||
142 | + | ||
143 | + openEditMode() { | ||
144 | + if (this.disabled) { | ||
145 | + return; | ||
146 | + } | ||
147 | + const isGtSm = this.breakpointObserver.isMatched(MediaBreakpoints['gt-sm']); | ||
148 | + const position = this.overlay.position(); | ||
149 | + const config = new OverlayConfig({ | ||
150 | + panelClass: 'tb-timewindow-panel', | ||
151 | + backdropClass: 'cdk-overlay-transparent-backdrop', | ||
152 | + hasBackdrop: isGtSm, | ||
153 | + }); | ||
154 | + if (isGtSm) { | ||
155 | + config.minWidth = '417px'; | ||
156 | + config.maxHeight = '440px'; | ||
157 | + const panelHeight = 375; | ||
158 | + const panelWidth = 417; | ||
159 | + const el = this.timewindowPanelOrigin.elementRef.nativeElement; | ||
160 | + const offset = el.getBoundingClientRect(); | ||
161 | + const scrollTop = this.window.pageYOffset || this.document.documentElement.scrollTop || this.document.body.scrollTop || 0; | ||
162 | + const scrollLeft = this.window.pageXOffset || this.document.documentElement.scrollLeft || this.document.body.scrollLeft || 0; | ||
163 | + const bottomY = offset.bottom - scrollTop; | ||
164 | + const leftX = offset.left - scrollLeft; | ||
165 | + let originX; | ||
166 | + let originY; | ||
167 | + let overlayX; | ||
168 | + let overlayY; | ||
169 | + const wHeight = this.document.documentElement.clientHeight; | ||
170 | + const wWidth = this.document.documentElement.clientWidth; | ||
171 | + if (bottomY + panelHeight > wHeight) { | ||
172 | + originY = 'top'; | ||
173 | + overlayY = 'bottom'; | ||
174 | + } else { | ||
175 | + originY = 'bottom'; | ||
176 | + overlayY = 'top'; | ||
177 | + } | ||
178 | + if (leftX + panelWidth > wWidth) { | ||
179 | + originX = 'end'; | ||
180 | + overlayX = 'end'; | ||
181 | + } else { | ||
182 | + originX = 'start'; | ||
183 | + overlayX = 'start'; | ||
184 | + } | ||
185 | + const connectedPosition: ConnectedPosition = { | ||
186 | + originX, | ||
187 | + originY, | ||
188 | + overlayX, | ||
189 | + overlayY | ||
190 | + }; | ||
191 | + config.positionStrategy = position.flexibleConnectedTo(this.timewindowPanelOrigin.elementRef) | ||
192 | + .withPositions([connectedPosition]); | ||
193 | + } else { | ||
194 | + config.minWidth = '100%'; | ||
195 | + config.minHeight = '100%'; | ||
196 | + config.positionStrategy = position.global().top('0%').left('0%') | ||
197 | + .right('0%').bottom('0%'); | ||
198 | + } | ||
199 | + | ||
200 | + const overlayRef = this.overlay.create(config); | ||
201 | + | ||
202 | + overlayRef.backdropClick().subscribe(() => { | ||
203 | + overlayRef.dispose(); | ||
204 | + }); | ||
205 | + | ||
206 | + const injector = this._createTimewindowPanelInjector( | ||
207 | + overlayRef, | ||
208 | + { | ||
209 | + timewindow: this.innerValue.clone(), | ||
210 | + historyOnly: this.historyOnly, | ||
211 | + aggregation: this.aggregation | ||
212 | + } | ||
213 | + ); | ||
214 | + | ||
215 | + const componentRef = overlayRef.attach(new ComponentPortal(TimewindowPanelComponent, this.viewContainerRef, injector)); | ||
216 | + componentRef.onDestroy(() => { | ||
217 | + if (componentRef.instance.result) { | ||
218 | + this.innerValue = componentRef.instance.result; | ||
219 | + this.updateDisplayValue(); | ||
220 | + this.notifyChanged(); | ||
221 | + } | ||
222 | + }); | ||
223 | + } | ||
224 | + | ||
225 | + private _createTimewindowPanelInjector(overlayRef: OverlayRef, data: TimewindowPanelData): PortalInjector { | ||
226 | + const injectionTokens = new WeakMap<any, any>([ | ||
227 | + [TIMEWINDOW_PANEL_DATA, data], | ||
228 | + [OverlayRef, overlayRef] | ||
229 | + ]); | ||
230 | + return new PortalInjector(this.viewContainerRef.injector, injectionTokens); | ||
231 | + } | ||
232 | + | ||
233 | + registerOnChange(fn: any): void { | ||
234 | + this.propagateChange = fn; | ||
235 | + } | ||
236 | + | ||
237 | + registerOnTouched(fn: any): void { | ||
238 | + } | ||
239 | + | ||
240 | + setDisabledState(isDisabled: boolean): void { | ||
241 | + this.disabled = isDisabled; | ||
242 | + } | ||
243 | + | ||
244 | + writeValue(obj: Timewindow): void { | ||
245 | + this.innerValue = Timewindow.initModelFromDefaultTimewindow(obj, this.timeService); | ||
246 | + this.updateDisplayValue(); | ||
247 | + } | ||
248 | + | ||
249 | + notifyChanged() { | ||
250 | + this.propagateChange(this.innerValue.cloneSelectedTimewindow()); | ||
251 | + } | ||
252 | + | ||
253 | + updateDisplayValue() { | ||
254 | + if (this.innerValue.selectedTab === TimewindowType.REALTIME && !this.historyOnly) { | ||
255 | + this.innerValue.displayValue = this.translate.instant('timewindow.realtime') + ' - ' + | ||
256 | + this.translate.instant('timewindow.last-prefix') + ' ' + | ||
257 | + this.millisecondsToTimeStringPipe.transform(this.innerValue.realtime.timewindowMs); | ||
258 | + } else { | ||
259 | + this.innerValue.displayValue = !this.historyOnly ? (this.translate.instant('timewindow.history') + ' - ') : ''; | ||
260 | + if (this.innerValue.history.historyType === HistoryWindowType.LAST_INTERVAL) { | ||
261 | + this.innerValue.displayValue += this.translate.instant('timewindow.last-prefix') + ' ' + | ||
262 | + this.millisecondsToTimeStringPipe.transform(this.innerValue.history.timewindowMs); | ||
263 | + } else { | ||
264 | + const startString = this.datePipe.transform(this.innerValue.history.fixedTimewindow.startTimeMs, 'yyyy-MM-dd HH:mm:ss'); | ||
265 | + const endString = this.datePipe.transform(this.innerValue.history.fixedTimewindow.endTimeMs, 'yyyy-MM-dd HH:mm:ss'); | ||
266 | + this.innerValue.displayValue += this.translate.instant('timewindow.period', {startTime: startString, endTime: endString}); | ||
267 | + } | ||
268 | + } | ||
269 | + } | ||
270 | + | ||
271 | + hideLabel() { | ||
272 | + return this.isToolbar && !this.breakpointObserver.isMatched(MediaBreakpoints['gt-md']); | ||
273 | + } | ||
274 | + | ||
275 | +} |
@@ -23,3 +23,9 @@ export interface Customer extends ContactBased<CustomerId> { | @@ -23,3 +23,9 @@ export interface Customer extends ContactBased<CustomerId> { | ||
23 | title: string; | 23 | title: string; |
24 | additionalInfo?: any; | 24 | additionalInfo?: any; |
25 | } | 25 | } |
26 | + | ||
27 | +export interface ShortCustomerInfo { | ||
28 | + customerId: CustomerId; | ||
29 | + title: string; | ||
30 | + isPublic: boolean; | ||
31 | +} |
1 | +/// | ||
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | ||
3 | +/// | ||
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | +/// you may not use this file except in compliance with the License. | ||
6 | +/// You may obtain a copy of the License at | ||
7 | +/// | ||
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | +/// | ||
10 | +/// Unless required by applicable law or agreed to in writing, software | ||
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | +/// See the License for the specific language governing permissions and | ||
14 | +/// limitations under the License. | ||
15 | +/// | ||
16 | + | ||
17 | +import {BaseData} from '@shared/models/base-data'; | ||
18 | +import {DashboardId} from '@shared/models/id/dashboard-id'; | ||
19 | +import {TenantId} from '@shared/models/id/tenant-id'; | ||
20 | +import {ShortCustomerInfo} from '@shared/models/customer.model'; | ||
21 | + | ||
22 | +export interface DashboardInfo extends BaseData<DashboardId> { | ||
23 | + tenantId: TenantId; | ||
24 | + title: string; | ||
25 | + assignedCustomers: Array<ShortCustomerInfo>; | ||
26 | +} | ||
27 | + | ||
28 | +export interface DashboardConfiguration { | ||
29 | + widgets: Array<any>; | ||
30 | + // TODO: | ||
31 | +} | ||
32 | + | ||
33 | +export interface Dashboard extends DashboardInfo { | ||
34 | + configuration: DashboardConfiguration; | ||
35 | +} |
1 | +/// | ||
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | ||
3 | +/// | ||
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | +/// you may not use this file except in compliance with the License. | ||
6 | +/// You may obtain a copy of the License at | ||
7 | +/// | ||
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | +/// | ||
10 | +/// Unless required by applicable law or agreed to in writing, software | ||
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | +/// See the License for the specific language governing permissions and | ||
14 | +/// limitations under the License. | ||
15 | +/// | ||
16 | + | ||
17 | +import { EntityId } from '@shared/models/id/entity-id'; | ||
18 | +import { PageLink } from '@shared/models/page/page-link'; | ||
19 | +import { BehaviorSubject, Observable, of, ReplaySubject } from 'rxjs'; | ||
20 | +import { emptyPageData, PageData } from '@shared/models/page/page-data'; | ||
21 | +import { BaseData, HasId } from '@shared/models/base-data'; | ||
22 | +import { CollectionViewer, DataSource } from '@angular/cdk/typings/collections'; | ||
23 | +import { catchError, map, take, tap } from 'rxjs/operators'; | ||
24 | +import { SelectionModel } from '@angular/cdk/collections'; | ||
25 | + | ||
26 | +export type EntitiesFetchFunction<T extends BaseData<HasId>, P extends PageLink> = (pageLink: P) => Observable<PageData<T>>; | ||
27 | + | ||
28 | +export class EntitiesDataSource<T extends BaseData<HasId>, P extends PageLink = PageLink> implements DataSource<T> { | ||
29 | + | ||
30 | + private entitiesSubject = new BehaviorSubject<T[]>([]); | ||
31 | + private pageDataSubject = new BehaviorSubject<PageData<T>>(emptyPageData<T>()); | ||
32 | + | ||
33 | + public pageData$ = this.pageDataSubject.asObservable(); | ||
34 | + | ||
35 | + public selection = new SelectionModel<T>(true, []); | ||
36 | + | ||
37 | + public currentEntity: T = null; | ||
38 | + | ||
39 | + constructor(private fetchFunction: EntitiesFetchFunction<T, P>) {} | ||
40 | + | ||
41 | + connect(collectionViewer: CollectionViewer): Observable<T[] | ReadonlyArray<T>> { | ||
42 | + return this.entitiesSubject.asObservable(); | ||
43 | + } | ||
44 | + | ||
45 | + disconnect(collectionViewer: CollectionViewer): void { | ||
46 | + this.entitiesSubject.complete(); | ||
47 | + this.pageDataSubject.complete(); | ||
48 | + } | ||
49 | + | ||
50 | + loadEntities(pageLink: P): Observable<PageData<T>> { | ||
51 | + const result = new ReplaySubject<PageData<T>>(); | ||
52 | + this.fetchFunction(pageLink).pipe( | ||
53 | + tap(() => { | ||
54 | + this.selection.clear(); | ||
55 | + }), | ||
56 | + catchError(() => of(emptyPageData<T>())), | ||
57 | + ).subscribe( | ||
58 | + (pageData) => { | ||
59 | + this.entitiesSubject.next(pageData.data); | ||
60 | + this.pageDataSubject.next(pageData); | ||
61 | + result.next(pageData); | ||
62 | + } | ||
63 | + ); | ||
64 | + return result; | ||
65 | + } | ||
66 | + | ||
67 | + isAllSelected(): Observable<boolean> { | ||
68 | + const numSelected = this.selection.selected.length; | ||
69 | + return this.entitiesSubject.pipe( | ||
70 | + map((entities) => numSelected === entities.length) | ||
71 | + ); | ||
72 | + } | ||
73 | + | ||
74 | + isEmpty(): Observable<boolean> { | ||
75 | + return this.entitiesSubject.pipe( | ||
76 | + map((entities) => !entities.length) | ||
77 | + ); | ||
78 | + } | ||
79 | + | ||
80 | + total(): Observable<number> { | ||
81 | + return this.pageDataSubject.pipe( | ||
82 | + map((pageData) => pageData.totalElements) | ||
83 | + ); | ||
84 | + } | ||
85 | + | ||
86 | + toggleCurrentEntity(entity: T): boolean { | ||
87 | + if (this.currentEntity !== entity) { | ||
88 | + this.currentEntity = entity; | ||
89 | + return true; | ||
90 | + } else { | ||
91 | + return false; | ||
92 | + } | ||
93 | + } | ||
94 | + | ||
95 | + isCurrentEntity(entity: T): boolean { | ||
96 | + return (this.currentEntity && entity && this.currentEntity.id && entity.id) && | ||
97 | + (this.currentEntity.id.id === entity.id.id); | ||
98 | + } | ||
99 | + | ||
100 | + masterToggle() { | ||
101 | + this.entitiesSubject.pipe( | ||
102 | + tap((entities) => { | ||
103 | + const numSelected = this.selection.selected.length; | ||
104 | + if (numSelected === entities.length) { | ||
105 | + this.selection.clear(); | ||
106 | + } else { | ||
107 | + entities.forEach(row => this.selection.select(row)); | ||
108 | + } | ||
109 | + }), | ||
110 | + take(1) | ||
111 | + ).subscribe(); | ||
112 | + } | ||
113 | +} |
1 | +/// | ||
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | ||
3 | +/// | ||
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | +/// you may not use this file except in compliance with the License. | ||
6 | +/// You may obtain a copy of the License at | ||
7 | +/// | ||
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | +/// | ||
10 | +/// Unless required by applicable law or agreed to in writing, software | ||
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | +/// See the License for the specific language governing permissions and | ||
14 | +/// limitations under the License. | ||
15 | +/// | ||
16 | + | ||
17 | +import { EntityId } from './entity-id'; | ||
18 | +import { EntityType } from '@shared/models/entity-type.models'; | ||
19 | + | ||
20 | +export class DashboardId implements EntityId { | ||
21 | + entityType = EntityType.DASHBOARD; | ||
22 | + id: string; | ||
23 | + constructor(id: string) { | ||
24 | + this.id = id; | ||
25 | + } | ||
26 | +} |
@@ -33,3 +33,20 @@ export interface MailServerSettings { | @@ -33,3 +33,20 @@ export interface MailServerSettings { | ||
33 | username: string; | 33 | username: string; |
34 | password: string; | 34 | password: string; |
35 | } | 35 | } |
36 | + | ||
37 | +export interface GeneralSettings { | ||
38 | + baseUrl: string; | ||
39 | +} | ||
40 | + | ||
41 | +export interface UserPasswordPolicy { | ||
42 | + minimumLength: number; | ||
43 | + minimumUppercaseLetters: number; | ||
44 | + minimumLowercaseLetters: number; | ||
45 | + minimumDigits: number; | ||
46 | + minimumSpecialCharacters: number; | ||
47 | + passwordExpirationPeriodDays: number; | ||
48 | +} | ||
49 | + | ||
50 | +export interface SecuritySettings { | ||
51 | + passwordPolicy: UserPasswordPolicy; | ||
52 | +} |
1 | +/// | ||
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | ||
3 | +/// | ||
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | +/// you may not use this file except in compliance with the License. | ||
6 | +/// You may obtain a copy of the License at | ||
7 | +/// | ||
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | +/// | ||
10 | +/// Unless required by applicable law or agreed to in writing, software | ||
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | +/// See the License for the specific language governing permissions and | ||
14 | +/// limitations under the License. | ||
15 | +/// | ||
16 | + | ||
17 | +import { Pipe, PipeTransform } from '@angular/core'; | ||
18 | + | ||
19 | +@Pipe({ | ||
20 | + name: 'enumToArray' | ||
21 | +}) | ||
22 | +export class EnumToArrayPipe implements PipeTransform { | ||
23 | + transform(data: object) { | ||
24 | + const keys = Object.keys(data); | ||
25 | + return keys.slice(keys.length / 2); | ||
26 | + } | ||
27 | +} |
ui-ngx/src/app/shared/pipe/highlight.pipe.ts
0 → 100644
1 | +/// | ||
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | ||
3 | +/// | ||
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | +/// you may not use this file except in compliance with the License. | ||
6 | +/// You may obtain a copy of the License at | ||
7 | +/// | ||
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | +/// | ||
10 | +/// Unless required by applicable law or agreed to in writing, software | ||
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | +/// See the License for the specific language governing permissions and | ||
14 | +/// limitations under the License. | ||
15 | +/// | ||
16 | + | ||
17 | +import {Pipe, PipeTransform} from '@angular/core'; | ||
18 | + | ||
19 | +@Pipe({ name: 'highlight' }) | ||
20 | +export class HighlightPipe implements PipeTransform { | ||
21 | + transform(text: string, search): string { | ||
22 | + const pattern = search | ||
23 | + .replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'); | ||
24 | + const regex = new RegExp('^' + pattern, 'i'); | ||
25 | + | ||
26 | + return search ? text.replace(regex, match => `<b>${match}</b>`) : text; | ||
27 | + } | ||
28 | +} |
1 | +/// | ||
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | ||
3 | +/// | ||
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | +/// you may not use this file except in compliance with the License. | ||
6 | +/// You may obtain a copy of the License at | ||
7 | +/// | ||
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | +/// | ||
10 | +/// Unless required by applicable law or agreed to in writing, software | ||
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | +/// See the License for the specific language governing permissions and | ||
14 | +/// limitations under the License. | ||
15 | +/// | ||
16 | + | ||
17 | +import { Pipe, PipeTransform } from '@angular/core'; | ||
18 | +import { TranslateService } from '@ngx-translate/core'; | ||
19 | + | ||
20 | +@Pipe({ | ||
21 | + name: 'milliSecondsToTimeString' | ||
22 | +}) | ||
23 | +export class MillisecondsToTimeStringPipe implements PipeTransform { | ||
24 | + | ||
25 | + constructor(private translate: TranslateService) { | ||
26 | + } | ||
27 | + | ||
28 | + transform(millseconds: number, args?: any): string { | ||
29 | + let seconds = Math.floor(millseconds / 1000); | ||
30 | + const days = Math.floor(seconds / 86400); | ||
31 | + let hours = Math.floor((seconds % 86400) / 3600); | ||
32 | + let minutes = Math.floor(((seconds % 86400) % 3600) / 60); | ||
33 | + seconds = seconds % 60; | ||
34 | + let timeString = ''; | ||
35 | + if (days > 0) { | ||
36 | + timeString += this.translate.instant('timewindow.days', {days}); | ||
37 | + } | ||
38 | + if (hours > 0) { | ||
39 | + if (timeString.length === 0 && hours === 1) { | ||
40 | + hours = 0; | ||
41 | + } | ||
42 | + timeString += this.translate.instant('timewindow.hours', {hours}); | ||
43 | + } | ||
44 | + if (minutes > 0) { | ||
45 | + if (timeString.length === 0 && minutes === 1) { | ||
46 | + minutes = 0; | ||
47 | + } | ||
48 | + timeString += this.translate.instant('timewindow.minutes', {minutes}); | ||
49 | + } | ||
50 | + if (seconds > 0) { | ||
51 | + if (timeString.length === 0 && seconds === 1) { | ||
52 | + seconds = 0; | ||
53 | + } | ||
54 | + timeString += this.translate.instant('timewindow.seconds', {seconds}); | ||
55 | + } | ||
56 | + return timeString; | ||
57 | + } | ||
58 | +} |
@@ -58,39 +58,41 @@ import { NospacePipe } from './pipe/nospace.pipe'; | @@ -58,39 +58,41 @@ import { NospacePipe } from './pipe/nospace.pipe'; | ||
58 | import { TranslateModule } from '@ngx-translate/core'; | 58 | import { TranslateModule } from '@ngx-translate/core'; |
59 | import { TbCheckboxComponent } from '@shared/components/tb-checkbox.component'; | 59 | import { TbCheckboxComponent } from '@shared/components/tb-checkbox.component'; |
60 | import { HelpComponent } from '@shared/components/help.component'; | 60 | import { HelpComponent } from '@shared/components/help.component'; |
61 | -// import { EntitiesTableComponent } from '@shared/components/entity/entities-table.component'; | ||
62 | -// import { AddEntityDialogComponent } from '@shared/components/entity/add-entity-dialog.component'; | ||
63 | -// import { DetailsPanelComponent } from '@shared/components/details-panel.component'; | ||
64 | -// import { EntityDetailsPanelComponent } from '@shared/components/entity/entity-details-panel.component'; | 61 | +import { EntitiesTableComponent } from '@shared/components/entity/entities-table.component'; |
62 | +import { AddEntityDialogComponent } from '@shared/components/entity/add-entity-dialog.component'; | ||
63 | +import { DetailsPanelComponent } from '@shared/components/details-panel.component'; | ||
64 | +import { EntityDetailsPanelComponent } from '@shared/components/entity/entity-details-panel.component'; | ||
65 | import { TbAnchorComponent } from '@shared/components/tb-anchor.component'; | 65 | import { TbAnchorComponent } from '@shared/components/tb-anchor.component'; |
66 | -// import { ContactComponent } from '@shared/components/contact.component'; | 66 | +import { ContactComponent } from '@shared/components/contact.component'; |
67 | // import { AuditLogDetailsDialogComponent } from '@shared/components/audit-log/audit-log-details-dialog.component'; | 67 | // import { AuditLogDetailsDialogComponent } from '@shared/components/audit-log/audit-log-details-dialog.component'; |
68 | // import { AuditLogTableComponent } from '@shared/components/audit-log/audit-log-table.component'; | 68 | // import { AuditLogTableComponent } from '@shared/components/audit-log/audit-log-table.component'; |
69 | -// import { MillisecondsToTimeStringPipe } from '@shared/pipe/milliseconds-to-time-string.pipe'; | ||
70 | -// import { TimewindowComponent } from '@shared/components/time/timewindow.component'; | 69 | +import { MillisecondsToTimeStringPipe } from '@shared/pipe/milliseconds-to-time-string.pipe'; |
70 | +import { TimewindowComponent } from '@shared/components/time/timewindow.component'; | ||
71 | import { OverlayModule } from '@angular/cdk/overlay'; | 71 | import { OverlayModule } from '@angular/cdk/overlay'; |
72 | -// import { TimewindowPanelComponent } from '@shared/components/time/timewindow-panel.component'; | ||
73 | -// import { TimeintervalComponent } from '@shared/components/time/timeinterval.component'; | ||
74 | -// import { DatetimePeriodComponent } from '@shared/components/time/datetime-period.component'; | ||
75 | -// import { EnumToArrayPipe } from '@shared/pipe/enum-to-array.pipe'; | 72 | +import { TimewindowPanelComponent } from '@shared/components/time/timewindow-panel.component'; |
73 | +import { TimeintervalComponent } from '@shared/components/time/timeinterval.component'; | ||
74 | +import { DatetimePeriodComponent } from '@shared/components/time/datetime-period.component'; | ||
75 | +import { EnumToArrayPipe } from '@shared/pipe/enum-to-array.pipe'; | ||
76 | import { ClipboardModule } from 'ngx-clipboard'; | 76 | import { ClipboardModule } from 'ngx-clipboard'; |
77 | // import { ValueInputComponent } from '@shared/components/value-input.component'; | 77 | // import { ValueInputComponent } from '@shared/components/value-input.component'; |
78 | -// import { IntervalCountPipe } from '@shared/pipe/interval-count.pipe'; | ||
79 | import { FullscreenDirective } from '@shared/components/fullscreen.directive'; | 78 | import { FullscreenDirective } from '@shared/components/fullscreen.directive'; |
79 | +import { HighlightPipe } from '@shared/pipe/highlight.pipe'; | ||
80 | +import {DashboardAutocompleteComponent} from '@shared/components/dashboard-autocomplete.component'; | ||
80 | 81 | ||
81 | @NgModule({ | 82 | @NgModule({ |
82 | providers: [ | 83 | providers: [ |
83 | DatePipe, | 84 | DatePipe, |
84 | -// MillisecondsToTimeStringPipe, | ||
85 | -// EnumToArrayPipe, | 85 | + MillisecondsToTimeStringPipe, |
86 | + EnumToArrayPipe, | ||
87 | + HighlightPipe | ||
86 | // IntervalCountPipe, | 88 | // IntervalCountPipe, |
87 | ], | 89 | ], |
88 | entryComponents: [ | 90 | entryComponents: [ |
89 | TbSnackBarComponent, | 91 | TbSnackBarComponent, |
90 | TbAnchorComponent, | 92 | TbAnchorComponent, |
91 | -// AddEntityDialogComponent, | 93 | + AddEntityDialogComponent, |
92 | // AuditLogDetailsDialogComponent, | 94 | // AuditLogDetailsDialogComponent, |
93 | -// TimewindowPanelComponent, | 95 | + TimewindowPanelComponent, |
94 | ], | 96 | ], |
95 | declarations: [ | 97 | declarations: [ |
96 | FooterComponent, | 98 | FooterComponent, |
@@ -103,22 +105,23 @@ import { FullscreenDirective } from '@shared/components/fullscreen.directive'; | @@ -103,22 +105,23 @@ import { FullscreenDirective } from '@shared/components/fullscreen.directive'; | ||
103 | TbSnackBarComponent, | 105 | TbSnackBarComponent, |
104 | BreadcrumbComponent, | 106 | BreadcrumbComponent, |
105 | UserMenuComponent, | 107 | UserMenuComponent, |
106 | -// EntitiesTableComponent, | ||
107 | -// AddEntityDialogComponent, | ||
108 | -// DetailsPanelComponent, | ||
109 | -// EntityDetailsPanelComponent, | ||
110 | -// ContactComponent, | 108 | + EntitiesTableComponent, |
109 | + AddEntityDialogComponent, | ||
110 | + DetailsPanelComponent, | ||
111 | + EntityDetailsPanelComponent, | ||
112 | + ContactComponent, | ||
111 | // AuditLogTableComponent, | 113 | // AuditLogTableComponent, |
112 | // AuditLogDetailsDialogComponent, | 114 | // AuditLogDetailsDialogComponent, |
113 | -// TimewindowComponent, | ||
114 | -// TimewindowPanelComponent, | ||
115 | -// TimeintervalComponent, | ||
116 | -// DatetimePeriodComponent, | 115 | + TimewindowComponent, |
116 | + TimewindowPanelComponent, | ||
117 | + TimeintervalComponent, | ||
118 | + DatetimePeriodComponent, | ||
117 | // ValueInputComponent, | 119 | // ValueInputComponent, |
120 | + DashboardAutocompleteComponent, | ||
118 | NospacePipe, | 121 | NospacePipe, |
119 | -// MillisecondsToTimeStringPipe, | ||
120 | -// EnumToArrayPipe, | ||
121 | -// IntervalCountPipe | 122 | + MillisecondsToTimeStringPipe, |
123 | + EnumToArrayPipe, | ||
124 | + HighlightPipe | ||
122 | ], | 125 | ], |
123 | imports: [ | 126 | imports: [ |
124 | CommonModule, | 127 | CommonModule, |
@@ -169,16 +172,17 @@ import { FullscreenDirective } from '@shared/components/fullscreen.directive'; | @@ -169,16 +172,17 @@ import { FullscreenDirective } from '@shared/components/fullscreen.directive'; | ||
169 | TbCheckboxComponent, | 172 | TbCheckboxComponent, |
170 | BreadcrumbComponent, | 173 | BreadcrumbComponent, |
171 | UserMenuComponent, | 174 | UserMenuComponent, |
172 | -// EntitiesTableComponent, | ||
173 | -// AddEntityDialogComponent, | ||
174 | -// DetailsPanelComponent, | ||
175 | -// EntityDetailsPanelComponent, | ||
176 | -// ContactComponent, | 175 | + EntitiesTableComponent, |
176 | + AddEntityDialogComponent, | ||
177 | + DetailsPanelComponent, | ||
178 | + EntityDetailsPanelComponent, | ||
179 | + ContactComponent, | ||
177 | // AuditLogTableComponent, | 180 | // AuditLogTableComponent, |
178 | -// TimewindowComponent, | ||
179 | -// TimewindowPanelComponent, | ||
180 | -// TimeintervalComponent, | ||
181 | -// DatetimePeriodComponent, | 181 | + TimewindowComponent, |
182 | + TimewindowPanelComponent, | ||
183 | + TimeintervalComponent, | ||
184 | + DatetimePeriodComponent, | ||
185 | + DashboardAutocompleteComponent, | ||
182 | // ValueInputComponent, | 186 | // ValueInputComponent, |
183 | MatButtonModule, | 187 | MatButtonModule, |
184 | MatCheckboxModule, | 188 | MatCheckboxModule, |
@@ -215,9 +219,9 @@ import { FullscreenDirective } from '@shared/components/fullscreen.directive'; | @@ -215,9 +219,9 @@ import { FullscreenDirective } from '@shared/components/fullscreen.directive'; | ||
215 | ReactiveFormsModule, | 219 | ReactiveFormsModule, |
216 | OverlayModule, | 220 | OverlayModule, |
217 | NospacePipe, | 221 | NospacePipe, |
218 | -// MillisecondsToTimeStringPipe, | ||
219 | -// EnumToArrayPipe, | ||
220 | -// IntervalCountPipe, | 222 | + MillisecondsToTimeStringPipe, |
223 | + EnumToArrayPipe, | ||
224 | + HighlightPipe, | ||
221 | TranslateModule | 225 | TranslateModule |
222 | ] | 226 | ] |
223 | }) | 227 | }) |
@@ -590,6 +590,7 @@ | @@ -590,6 +590,7 @@ | ||
590 | "add-datasource-prompt": "Please add datasource" | 590 | "add-datasource-prompt": "Please add datasource" |
591 | }, | 591 | }, |
592 | "details": { | 592 | "details": { |
593 | + "details": "Details", | ||
593 | "edit-mode": "Edit mode", | 594 | "edit-mode": "Edit mode", |
594 | "toggle-edit-mode": "Toggle edit mode" | 595 | "toggle-edit-mode": "Toggle edit mode" |
595 | }, | 596 | }, |
@@ -1378,6 +1379,7 @@ | @@ -1378,6 +1379,7 @@ | ||
1378 | "delete-tenants-title": "Are you sure you want to delete { count, plural, 1 {1 tenant} other {# tenants} }?", | 1379 | "delete-tenants-title": "Are you sure you want to delete { count, plural, 1 {1 tenant} other {# tenants} }?", |
1379 | "delete-tenants-action-title": "Delete { count, plural, 1 {1 tenant} other {# tenants} }", | 1380 | "delete-tenants-action-title": "Delete { count, plural, 1 {1 tenant} other {# tenants} }", |
1380 | "delete-tenants-text": "Be careful, after the confirmation all selected tenants will be removed and all related data will become unrecoverable.", | 1381 | "delete-tenants-text": "Be careful, after the confirmation all selected tenants will be removed and all related data will become unrecoverable.", |
1382 | + "created-time": "Created time", | ||
1381 | "title": "Title", | 1383 | "title": "Title", |
1382 | "title-required": "Title is required.", | 1384 | "title-required": "Title is required.", |
1383 | "description": "Description", | 1385 | "description": "Description", |
@@ -1437,6 +1439,7 @@ | @@ -1437,6 +1439,7 @@ | ||
1437 | "delete-users-text": "Be careful, after the confirmation all selected users will be removed and all related data will become unrecoverable.", | 1439 | "delete-users-text": "Be careful, after the confirmation all selected users will be removed and all related data will become unrecoverable.", |
1438 | "activation-email-sent-message": "Activation email was successfully sent!", | 1440 | "activation-email-sent-message": "Activation email was successfully sent!", |
1439 | "resend-activation": "Resend activation", | 1441 | "resend-activation": "Resend activation", |
1442 | + "created-time": "Created time", | ||
1440 | "email": "Email", | 1443 | "email": "Email", |
1441 | "email-required": "Email is required.", | 1444 | "email-required": "Email is required.", |
1442 | "invalid-email-format": "Invalid email format.", | 1445 | "invalid-email-format": "Invalid email format.", |
@@ -152,7 +152,7 @@ | @@ -152,7 +152,7 @@ | ||
152 | "aknowledge-alarm-title": "Reconocer alarma", | 152 | "aknowledge-alarm-title": "Reconocer alarma", |
153 | "aknowledge-alarm-text": "¿Está seguro que quiere reconocer la alarma?", | 153 | "aknowledge-alarm-text": "¿Está seguro que quiere reconocer la alarma?", |
154 | "clear-alarms-title": "Quitar { count, plural, 1 {1 alarma} other {# alarmas} }", | 154 | "clear-alarms-title": "Quitar { count, plural, 1 {1 alarma} other {# alarmas} }", |
155 | - "clear-alarms-text": "¿Está seguro de que desea quitar { count, plural, 1 {1 alarma} other {# alarmas}?", | 155 | + "clear-alarms-text": "¿Está seguro de que desea quitar { count, plural, 1 {1 alarma} other {# alarmas} }?", |
156 | "clear-alarm-title": "Quitar alarma", | 156 | "clear-alarm-title": "Quitar alarma", |
157 | "clear-alarm-text": "¿Está seguro que quiere quitar la alarma?", | 157 | "clear-alarm-text": "¿Está seguro que quiere quitar la alarma?", |
158 | "alarm-status-filter": "Filtro de estado de alarma" | 158 | "alarm-status-filter": "Filtro de estado de alarma" |
@@ -88,16 +88,16 @@ | @@ -88,16 +88,16 @@ | ||
88 | "alarm": { | 88 | "alarm": { |
89 | "ack-time": "Heure d'acquittement", | 89 | "ack-time": "Heure d'acquittement", |
90 | "acknowledge": "Acquitter", | 90 | "acknowledge": "Acquitter", |
91 | - "aknowledge-alarms-text": "Etes-vous sûr de vouloir acquitter {count, plural, 1 {1 alarme} other {# alarmes}}?", | ||
92 | - "aknowledge-alarms-title": "Acquitter {count, plural, 1 {1 alarme} other {# alarmes}}", | 91 | + "aknowledge-alarms-text": "Etes-vous sûr de vouloir acquitter { count, plural, 1 {1 alarme} other {# alarmes} }?", |
92 | + "aknowledge-alarms-title": "Acquitter { count, plural, 1 {1 alarme} other {# alarmes} }", | ||
93 | "alarm": "Alarme", | 93 | "alarm": "Alarme", |
94 | "alarm-details": "Détails de l'alarme", | 94 | "alarm-details": "Détails de l'alarme", |
95 | "alarm-required": "Une alarme est requise", | 95 | "alarm-required": "Une alarme est requise", |
96 | "alarm-status": "Etat d'alarme", | 96 | "alarm-status": "Etat d'alarme", |
97 | "alarms": "Alarmes", | 97 | "alarms": "Alarmes", |
98 | "clear": "Effacer", | 98 | "clear": "Effacer", |
99 | - "clear-alarms-text": "Êtes-vous sûr de vouloir effacer {count, plural, 1 {1 alarme} other {# alarmes}}?", | ||
100 | - "clear-alarms-title": "Effacer {count, plural, 1 {1 alarme} other {# alarmes}}", | 99 | + "clear-alarms-text": "Êtes-vous sûr de vouloir effacer { count, plural, 1 {1 alarme} other {# alarmes} }?", |
100 | + "clear-alarms-title": "Effacer { count, plural, 1 {1 alarme} other {# alarmes} }", | ||
101 | "clear-time": "Heure d'éffacement", | 101 | "clear-time": "Heure d'éffacement", |
102 | "created-time": "Heure de création", | 102 | "created-time": "Heure de création", |
103 | "details": "Détails", | 103 | "details": "Détails", |
@@ -125,7 +125,7 @@ | @@ -125,7 +125,7 @@ | ||
125 | "UNACK": "non acquittée" | 125 | "UNACK": "non acquittée" |
126 | }, | 126 | }, |
127 | "select-alarm": "Sélectionnez une alarme", | 127 | "select-alarm": "Sélectionnez une alarme", |
128 | - "selected-alarms": "{count, plural, 1 {1 alarme} other {# alarmes}} sélectionnées", | 128 | + "selected-alarms": "{ count, plural, 1 {1 alarme} other {# alarmes} } sélectionnées", |
129 | "severity": "Gravitée", | 129 | "severity": "Gravitée", |
130 | "severity-critical": "Critique", | 130 | "severity-critical": "Critique", |
131 | "severity-indeterminate": "indéterminée", | 131 | "severity-indeterminate": "indéterminée", |
@@ -192,7 +192,7 @@ | @@ -192,7 +192,7 @@ | ||
192 | "assign-asset-to-customer": "Attribuer des Assets au client", | 192 | "assign-asset-to-customer": "Attribuer des Assets au client", |
193 | "assign-asset-to-customer-text": "Veuillez sélectionner les Assets à attribuer au client", | 193 | "assign-asset-to-customer-text": "Veuillez sélectionner les Assets à attribuer au client", |
194 | "assign-assets": "Attribuer des Assets", | 194 | "assign-assets": "Attribuer des Assets", |
195 | - "assign-assets-text": "Attribuer {count, plural, 1 {1 asset} other {# assets}} au client", | 195 | + "assign-assets-text": "Attribuer { count, plural, 1 {1 asset} other {# assets} } au client", |
196 | "assign-new-asset": "Attribuer un nouvel Asset", | 196 | "assign-new-asset": "Attribuer un nouvel Asset", |
197 | "assign-to-customer": "Attribuer au client", | 197 | "assign-to-customer": "Attribuer au client", |
198 | "assign-to-customer-text": "Veuillez sélectionner le client pour attribuer le ou les Assets", | 198 | "assign-to-customer-text": "Veuillez sélectionner le client pour attribuer le ou les Assets", |
@@ -202,9 +202,9 @@ | @@ -202,9 +202,9 @@ | ||
202 | "delete-asset-text": "Faites attention, après la confirmation, l'Asset et toutes les données associées deviendront irrécupérables.", | 202 | "delete-asset-text": "Faites attention, après la confirmation, l'Asset et toutes les données associées deviendront irrécupérables.", |
203 | "delete-asset-title": "Êtes-vous sûr de vouloir supprimer l'Asset '{{assetName}}'?", | 203 | "delete-asset-title": "Êtes-vous sûr de vouloir supprimer l'Asset '{{assetName}}'?", |
204 | "delete-assets": "Supprimer des Assets", | 204 | "delete-assets": "Supprimer des Assets", |
205 | - "delete-assets-action-title": "Supprimer {count, plural, 1 {1 asset} other {# assets}}", | 205 | + "delete-assets-action-title": "Supprimer { count, plural, 1 {1 asset} other {# assets} }", |
206 | "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.", | 206 | "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.", |
207 | - "delete-assets-title": "Etes-vous sûr de vouloir supprimer {count, plural, 1 {1 asset} other {# assets}}?", | 207 | + "delete-assets-title": "Etes-vous sûr de vouloir supprimer { count, plural, 1 {1 asset} other {# assets} }?", |
208 | "description": "Description", | 208 | "description": "Description", |
209 | "details": "Détails", | 209 | "details": "Détails", |
210 | "enter-asset-type": "Entrez le type d'Asset", | 210 | "enter-asset-type": "Entrez le type d'Asset", |
@@ -232,9 +232,9 @@ | @@ -232,9 +232,9 @@ | ||
232 | "unassign-asset-text": "Après la confirmation, l'Asset sera non attribué et ne sera pas accessible au client.", | 232 | "unassign-asset-text": "Après la confirmation, l'Asset sera non attribué et ne sera pas accessible au client.", |
233 | "unassign-asset-title": "Êtes-vous sûr de vouloir retirer l'attribution de l'Asset '{{assetName}}'?", | 233 | "unassign-asset-title": "Êtes-vous sûr de vouloir retirer l'attribution de l'Asset '{{assetName}}'?", |
234 | "unassign-assets": "Retirer les Assets", | 234 | "unassign-assets": "Retirer les Assets", |
235 | - "unassign-assets-action-title": "Retirer {count, plural, 1 {1 asset} other {# assets}} du client", | 235 | + "unassign-assets-action-title": "Retirer { count, plural, 1 {1 asset} other {# assets} } du client", |
236 | "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.", | 236 | "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.", |
237 | - "unassign-assets-title": "Êtes-vous sûr de vouloir retirer l'attribution de {count, plural, 1 {1 asset} other {# assets}}?", | 237 | + "unassign-assets-title": "Êtes-vous sûr de vouloir retirer l'attribution de { count, plural, 1 {1 asset} other {# assets} }?", |
238 | "unassign-from-customer": "Retirer du client", | 238 | "unassign-from-customer": "Retirer du client", |
239 | "view-assets": "Afficher les Assets" | 239 | "view-assets": "Afficher les Assets" |
240 | }, | 240 | }, |
@@ -246,7 +246,7 @@ | @@ -246,7 +246,7 @@ | ||
246 | "attributes-scope": "Etendue des attributs d'entité", | 246 | "attributes-scope": "Etendue des attributs d'entité", |
247 | "delete-attributes": "Supprimer les attributs", | 247 | "delete-attributes": "Supprimer les attributs", |
248 | "delete-attributes-text": "Attention, après la confirmation, tous les attributs sélectionnés seront supprimés.", | 248 | "delete-attributes-text": "Attention, après la confirmation, tous les attributs sélectionnés seront supprimés.", |
249 | - "delete-attributes-title": "Êtes-vous sûr de vouloir supprimer {count, plural, 1 {1 attribut} other {# attributs}}?", | 249 | + "delete-attributes-title": "Êtes-vous sûr de vouloir supprimer { count, plural, 1 {1 attribut} other {# attributs} }?", |
250 | "enter-attribute-value": "Entrez la valeur de l'attribut", | 250 | "enter-attribute-value": "Entrez la valeur de l'attribut", |
251 | "key": "Clé", | 251 | "key": "Clé", |
252 | "key-required": "La Clé d'attribut est requise.", | 252 | "key-required": "La Clé d'attribut est requise.", |
@@ -258,8 +258,8 @@ | @@ -258,8 +258,8 @@ | ||
258 | "scope-latest-telemetry": "Dernière télémétrie", | 258 | "scope-latest-telemetry": "Dernière télémétrie", |
259 | "scope-server": "Attributs du serveur", | 259 | "scope-server": "Attributs du serveur", |
260 | "scope-shared": "Attributs partagés", | 260 | "scope-shared": "Attributs partagés", |
261 | - "selected-attributes": "{count, plural, 1 {1 attribut} other {# attributs}} sélectionnés", | ||
262 | - "selected-telemetry": "{count, plural, 1 {1 unité de télémétrie} other {# unités de télémétrie}} sélectionnées", | 261 | + "selected-attributes": "{ count, plural, 1 {1 attribut} other {# attributs} } sélectionnés", |
262 | + "selected-telemetry": "{ count, plural, 1 {1 unité de télémétrie} other {# unités de télémétrie} } sélectionnées", | ||
263 | "show-on-widget": "Afficher sur le widget", | 263 | "show-on-widget": "Afficher sur le widget", |
264 | "value": "Valeur", | 264 | "value": "Valeur", |
265 | "value-required": "La valeur d'attribut est obligatoire.", | 265 | "value-required": "La valeur d'attribut est obligatoire.", |
@@ -358,9 +358,9 @@ | @@ -358,9 +358,9 @@ | ||
358 | "delete": "Supprimer le client", | 358 | "delete": "Supprimer le client", |
359 | "delete-customer-text": "Faites attention, après la confirmation, le client et toutes les données associées deviendront irrécupérables.", | 359 | "delete-customer-text": "Faites attention, après la confirmation, le client et toutes les données associées deviendront irrécupérables.", |
360 | "delete-customer-title": "Êtes-vous sûr de vouloir supprimer le client '{{customerTitle}}'?", | 360 | "delete-customer-title": "Êtes-vous sûr de vouloir supprimer le client '{{customerTitle}}'?", |
361 | - "delete-customers-action-title": "Supprimer {count, plural, 1 {1 client} other {# clients}}", | 361 | + "delete-customers-action-title": "Supprimer { count, plural, 1 {1 client} other {# clients} }", |
362 | "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.", | 362 | "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.", |
363 | - "delete-customers-title": "Êtes-vous sûr de vouloir supprimer {count, plural, 1 {1 client} other {# clients}}?", | 363 | + "delete-customers-title": "Êtes-vous sûr de vouloir supprimer { count, plural, 1 {1 client} other {# clients} }?", |
364 | "description": "Description", | 364 | "description": "Description", |
365 | "details": "Détails", | 365 | "details": "Détails", |
366 | "devices": "Dispositifs du client", | 366 | "devices": "Dispositifs du client", |
@@ -397,7 +397,7 @@ | @@ -397,7 +397,7 @@ | ||
397 | "assign-dashboard-to-customer": "Attribuer des tableaux de bord au client", | 397 | "assign-dashboard-to-customer": "Attribuer des tableaux de bord au client", |
398 | "assign-dashboard-to-customer-text": "Veuillez sélectionner les tableaux de bord à affecter au client", | 398 | "assign-dashboard-to-customer-text": "Veuillez sélectionner les tableaux de bord à affecter au client", |
399 | "assign-dashboards": "Attribuer des tableaux de bord", | 399 | "assign-dashboards": "Attribuer des tableaux de bord", |
400 | - "assign-dashboards-text": "Attribuer {count, plural, 1 {1 tableau de bord} other {# tableaux de bord}} aux clients", | 400 | + "assign-dashboards-text": "Attribuer { count, plural, 1 {1 tableau de bord} other {# tableaux de bord} } aux clients", |
401 | "assign-new-dashboard": "Attribuer un nouveau tableau de bord", | 401 | "assign-new-dashboard": "Attribuer un nouveau tableau de bord", |
402 | "assign-to-customer": "Attribuer au client", | 402 | "assign-to-customer": "Attribuer au client", |
403 | "assign-to-customer-text": "Veuillez sélectionner le client pour attribuer le ou les tableaux de bord", | 403 | "assign-to-customer-text": "Veuillez sélectionner le client pour attribuer le ou les tableaux de bord", |
@@ -428,9 +428,9 @@ | @@ -428,9 +428,9 @@ | ||
428 | "delete-dashboard-text": "Faites attention, après la confirmation, le tableau de bord et toutes les données associées deviendront irrécupérables.", | 428 | "delete-dashboard-text": "Faites attention, après la confirmation, le tableau de bord et toutes les données associées deviendront irrécupérables.", |
429 | "delete-dashboard-title": "Êtes-vous sûr de vouloir supprimer le tableau de bord '{{dashboardTitle}}'?", | 429 | "delete-dashboard-title": "Êtes-vous sûr de vouloir supprimer le tableau de bord '{{dashboardTitle}}'?", |
430 | "delete-dashboards": "Supprimer les tableaux de bord", | 430 | "delete-dashboards": "Supprimer les tableaux de bord", |
431 | - "delete-dashboards-action-title": "Supprimer {count, plural, 1 {1 tableau de bord} other {# tableaux de bord}}", | 431 | + "delete-dashboards-action-title": "Supprimer {count, plural, 1 {1 tableau de bord} other {# tableaux de bord} }", |
432 | "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.", | 432 | "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.", |
433 | - "delete-dashboards-title": "Voulez-vous vraiment supprimer {count, plural, 1 {1 tableau de bord} other {# tableaux de bord}}?", | 433 | + "delete-dashboards-title": "Voulez-vous vraiment supprimer {count, plural, 1 {1 tableau de bord} other {# tableaux de bord} }?", |
434 | "delete-state": "Supprimer l'état du tableau de bord", | 434 | "delete-state": "Supprimer l'état du tableau de bord", |
435 | "delete-state-text": "Etes-vous sûr de vouloir supprimer l'état du tableau de bord avec le nom '{{stateName}}'?", | 435 | "delete-state-text": "Etes-vous sûr de vouloir supprimer l'état du tableau de bord avec le nom '{{stateName}}'?", |
436 | "delete-state-title": "Supprimer l'état du tableau de bord", | 436 | "delete-state-title": "Supprimer l'état du tableau de bord", |
@@ -493,7 +493,7 @@ | @@ -493,7 +493,7 @@ | ||
493 | "select-state": "Sélectionnez l'état cible", | 493 | "select-state": "Sélectionnez l'état cible", |
494 | "select-widget-subtitle": "Liste des types de widgets disponibles", | 494 | "select-widget-subtitle": "Liste des types de widgets disponibles", |
495 | "select-widget-title": "Sélectionner un widget", | 495 | "select-widget-title": "Sélectionner un widget", |
496 | - "selected-states": "{count, plural, 1 {1 état du tableau de bord} other {# états du tableau de bord}} sélectionnés", | 496 | + "selected-states": "{count, plural, 1 {1 état du tableau de bord} other {# états du tableau de bord} } sélectionnés", |
497 | "set-background": "Définir l'arrière-plan", | 497 | "set-background": "Définir l'arrière-plan", |
498 | "settings": "Paramètres", | 498 | "settings": "Paramètres", |
499 | "show-details": "Afficher les détails", | 499 | "show-details": "Afficher les détails", |
@@ -515,10 +515,10 @@ | @@ -515,10 +515,10 @@ | ||
515 | "unassign-dashboard-text": "Après la confirmation, le tableau de bord ne sera pas attribué et ne sera pas accessible au client.", | 515 | "unassign-dashboard-text": "Après la confirmation, le tableau de bord ne sera pas attribué et ne sera pas accessible au client.", |
516 | "unassign-dashboard-title": "Êtes-vous sûr de vouloir annuler l'affectation du tableau de bord '{{dashboardTitle}}'?", | 516 | "unassign-dashboard-title": "Êtes-vous sûr de vouloir annuler l'affectation du tableau de bord '{{dashboardTitle}}'?", |
517 | "unassign-dashboards": "Retirer les tableaux de bord", | 517 | "unassign-dashboards": "Retirer les tableaux de bord", |
518 | - "unassign-dashboards-action-text": "Annuler l'affectation {count, plural, 1 {1 tableau de bord} other {# tableaux de bord}} des clients", | ||
519 | - "unassign-dashboards-action-title": "Annuler l'affectation {count, plural, 1 {1 tableau de bord} other {# tableaux de bord}} du client", | 518 | + "unassign-dashboards-action-text": "Annuler l'affectation {count, plural, 1 {1 tableau de bord} other {# tableaux de bord} } des clients", |
519 | + "unassign-dashboards-action-title": "Annuler l'affectation {count, plural, 1 {1 tableau de bord} other {# tableaux de bord} } du client", | ||
520 | "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.", | 520 | "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.", |
521 | - "unassign-dashboards-title": "Etes-vous sûr de vouloir annuler l'affectation {count, plural, 1 {1 tableau de bord} other {# tableaux de bord}}?", | 521 | + "unassign-dashboards-title": "Etes-vous sûr de vouloir annuler l'affectation {count, plural, 1 {1 tableau de bord} other {# tableaux de bord} }?", |
522 | "unassign-from-customer": "Retirer du client", | 522 | "unassign-from-customer": "Retirer du client", |
523 | "unassign-from-customers": "Retirer les tableaux de bord des clients", | 523 | "unassign-from-customers": "Retirer les tableaux de bord des clients", |
524 | "unassign-from-customers-text": "Veuillez sélectionner les clients à annuler l'affectation du ou des tableaux de bord", | 524 | "unassign-from-customers-text": "Veuillez sélectionner les clients à annuler l'affectation du ou des tableaux de bord", |
@@ -541,8 +541,8 @@ | @@ -541,8 +541,8 @@ | ||
541 | "function-types": "Types de fonctions", | 541 | "function-types": "Types de fonctions", |
542 | "function-types-required": "Les types de fonctions sont obligatoires", | 542 | "function-types-required": "Les types de fonctions sont obligatoires", |
543 | "label": "Label", | 543 | "label": "Label", |
544 | - "maximum-function-types": "Maximum {count, plural, 1 {1 type de fonction est autorisé.} other {# types de fonctions sont autorisés}}", | ||
545 | - "maximum-timeseries-or-attributes": "Maximum {count, plural, 1 {1 timeseries / attribut est autorisé.} other {# timeseries / attributs sont autorisés}}", | 544 | + "maximum-function-types": "Maximum {count, plural, 1 {1 type de fonction est autorisé.} other {# types de fonctions sont autorisés} }", |
545 | + "maximum-timeseries-or-attributes": "Maximum {count, plural, 1 {1 timeseries / attribut est autorisé.} other {# timeseries / attributs sont autorisés} }", | ||
546 | "settings": "Paramètres", | 546 | "settings": "Paramètres", |
547 | "timeseries": "Timeseries", | 547 | "timeseries": "Timeseries", |
548 | "timeseries-or-attributes-required": "Les timeseries / attributs d'entité sont obligatoires.", | 548 | "timeseries-or-attributes-required": "Les timeseries / attributs d'entité sont obligatoires.", |
@@ -580,7 +580,7 @@ | @@ -580,7 +580,7 @@ | ||
580 | "assign-device-to-customer": "Affecter des dispositifs au client", | 580 | "assign-device-to-customer": "Affecter des dispositifs au client", |
581 | "assign-device-to-customer-text": "Veuillez sélectionner les dispositif à affecter au client", | 581 | "assign-device-to-customer-text": "Veuillez sélectionner les dispositif à affecter au client", |
582 | "assign-devices": "Attribuer des dispositifs", | 582 | "assign-devices": "Attribuer des dispositifs", |
583 | - "assign-devices-text": "Attribuer {count, plural, 1 {1 dispositif} other {# dispositifs}} au client", | 583 | + "assign-devices-text": "Attribuer {count, plural, 1 {1 dispositif} other {# dispositifs} } au client", |
584 | "assign-new-device": "Attribuer un nouveau dispositif", | 584 | "assign-new-device": "Attribuer un nouveau dispositif", |
585 | "assign-to-customer": "Attribuer au client", | 585 | "assign-to-customer": "Attribuer au client", |
586 | "assign-to-customer-text": "Veuillez sélectionner le client pour attribuer le ou les dispositifs", | 586 | "assign-to-customer-text": "Veuillez sélectionner le client pour attribuer le ou les dispositifs", |
@@ -596,9 +596,9 @@ | @@ -596,9 +596,9 @@ | ||
596 | "delete-device-text": "Faites attention, après la confirmation, le dispositif et toutes les données associées deviendront irrécupérables.", | 596 | "delete-device-text": "Faites attention, après la confirmation, le dispositif et toutes les données associées deviendront irrécupérables.", |
597 | "delete-device-title": "Êtes-vous sûr de vouloir supprimer le dispositif '{{deviceName}}'?", | 597 | "delete-device-title": "Êtes-vous sûr de vouloir supprimer le dispositif '{{deviceName}}'?", |
598 | "delete-devices": "Supprimer les dispositifs", | 598 | "delete-devices": "Supprimer les dispositifs", |
599 | - "delete-devices-action-title": "Supprimer {count, plural, 1 {1 dispositif} other {# dispositifs}}", | 599 | + "delete-devices-action-title": "Supprimer {count, plural, 1 {1 dispositif} other {# dispositifs} }", |
600 | "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.", | 600 | "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.", |
601 | - "delete-devices-title": "Êtes-vous sûr de vouloir supprimer {count, plural, 1 {1 dispositif} other {# dispositifs}}?", | 601 | + "delete-devices-title": "Êtes-vous sûr de vouloir supprimer {count, plural, 1 {1 dispositif} other {# dispositifs} }?", |
602 | "description": "Description", | 602 | "description": "Description", |
603 | "details": "Détails", | 603 | "details": "Détails", |
604 | "device": "Dispositif", | 604 | "device": "Dispositif", |
@@ -653,9 +653,9 @@ | @@ -653,9 +653,9 @@ | ||
653 | "unassign-device-text": "Après la confirmation, le dispositif ne sera pas attribué et ne sera pas accessible au client.", | 653 | "unassign-device-text": "Après la confirmation, le dispositif ne sera pas attribué et ne sera pas accessible au client.", |
654 | "unassign-device-title": "Êtes-vous sûr de vouloir annuler l'affection du dispositif {{deviceName}} '?", | 654 | "unassign-device-title": "Êtes-vous sûr de vouloir annuler l'affection du dispositif {{deviceName}} '?", |
655 | "unassign-devices": "Annuler l'affectation des dispositifs", | 655 | "unassign-devices": "Annuler l'affectation des dispositifs", |
656 | - "unassign-devices-action-title": "Annuler l'affectation de {count, plural, 1 {1 dispositif} other {#dispositifs}} du client", | 656 | + "unassign-devices-action-title": "Annuler l'affectation de {count, plural, 1 {1 dispositif} other {#dispositifs} } du client", |
657 | "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.", | 657 | "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.", |
658 | - "unassign-devices-title": "Voulez-vous vraiment annuler l'affectation de {count, plural, 1 {1 dispositif} other {# dispositifs}}?", | 658 | + "unassign-devices-title": "Voulez-vous vraiment annuler l'affectation de {count, plural, 1 {1 dispositif} other {# dispositifs} }?", |
659 | "unassign-from-customer": "Retirer du client", | 659 | "unassign-from-customer": "Retirer du client", |
660 | "use-device-name-filter": "Utiliser le filtre", | 660 | "use-device-name-filter": "Utiliser le filtre", |
661 | "view-credentials": "Afficher les informations d'identification", | 661 | "view-credentials": "Afficher les informations d'identification", |
@@ -696,17 +696,17 @@ | @@ -696,17 +696,17 @@ | ||
696 | "entity-types": "Types d'entité", | 696 | "entity-types": "Types d'entité", |
697 | "key": "Clé", | 697 | "key": "Clé", |
698 | "key-name": "Nom de la clé", | 698 | "key-name": "Nom de la clé", |
699 | - "list-of-alarms": "{count, plural, 1 {Une alarme} other {Liste de # alarmes}}", | ||
700 | - "list-of-assets": "{count, plural, 1 {Un Asset} other {Liste de # Assets}}", | ||
701 | - "list-of-customers": "{count, plural, 1 {Un client} other {Liste de # clients}}", | ||
702 | - "list-of-dashboards": "{count, plural, 1 {Un tableau de bord} other {Liste de # tableaux de bord}}", | ||
703 | - "list-of-devices": "{count, plural, 1 {Un dispositif} other {Liste de # dispositifs}}", | ||
704 | - "list-of-plugins": "{count, plural, 1 {Un plugin} other {Liste de # plugins}}", | ||
705 | - "list-of-rulechains": "{count, plural, 1 {Une chaîne de règles} other {Liste de # chaînes de règles}}", | ||
706 | - "list-of-rulenodes": "{count, plural, 1 {Un noeud de règles} other {Liste de # noeuds de règles}}", | ||
707 | - "list-of-rules": "{count, plural, 1 {Une règle} other {Liste de # règles}}", | ||
708 | - "list-of-tenants": "{count, plural, 1 {Un tenant} other {Liste de # tenants}}", | ||
709 | - "list-of-users": "{count, plural, 1 {Un utilisateur} other {Liste de # utilisateurs}}", | 699 | + "list-of-alarms": "{count, plural, 1 {Une alarme} other {Liste de # alarmes} }", |
700 | + "list-of-assets": "{count, plural, 1 {Un Asset} other {Liste de # Assets} }", | ||
701 | + "list-of-customers": "{count, plural, 1 {Un client} other {Liste de # clients} }", | ||
702 | + "list-of-dashboards": "{count, plural, 1 {Un tableau de bord} other {Liste de # tableaux de bord} }", | ||
703 | + "list-of-devices": "{count, plural, 1 {Un dispositif} other {Liste de # dispositifs} }", | ||
704 | + "list-of-plugins": "{count, plural, 1 {Un plugin} other {Liste de # plugins} }", | ||
705 | + "list-of-rulechains": "{count, plural, 1 {Une chaîne de règles} other {Liste de # chaînes de règles} }", | ||
706 | + "list-of-rulenodes": "{count, plural, 1 {Un noeud de règles} other {Liste de # noeuds de règles} }", | ||
707 | + "list-of-rules": "{count, plural, 1 {Une règle} other {Liste de # règles} }", | ||
708 | + "list-of-tenants": "{count, plural, 1 {Un tenant} other {Liste de # tenants} }", | ||
709 | + "list-of-users": "{count, plural, 1 {Un utilisateur} other {Liste de # utilisateurs} }", | ||
710 | "missing-entity-filter-error": "Le filtre est manquant pour l'alias '{{alias}}'.", | 710 | "missing-entity-filter-error": "Le filtre est manquant pour l'alias '{{alias}}'.", |
711 | "name-starts-with": "Nom commence par", | 711 | "name-starts-with": "Nom commence par", |
712 | "no-alias-matching": "'{{alias}}' introuvable.", | 712 | "no-alias-matching": "'{{alias}}' introuvable.", |
@@ -724,7 +724,7 @@ | @@ -724,7 +724,7 @@ | ||
724 | "rulenode-name-starts-with": "Les noeuds de règles dont le nom commence par '{{prefix}}'", | 724 | "rulenode-name-starts-with": "Les noeuds de règles dont le nom commence par '{{prefix}}'", |
725 | "search": "Recherche d'entités", | 725 | "search": "Recherche d'entités", |
726 | "select-entities": "Sélectionner des entités", | 726 | "select-entities": "Sélectionner des entités", |
727 | - "selected-entities": "{count, plural, 1 {1 entité} other {# entités}} sélectionnées", | 727 | + "selected-entities": "{count, plural, 1 {1 entité} other {# entités} } sélectionnées", |
728 | "tenant-name-starts-with": "Les Tenant dont le nom commence par '{{prefix}}'", | 728 | "tenant-name-starts-with": "Les Tenant dont le nom commence par '{{prefix}}'", |
729 | "type": "Type", | 729 | "type": "Type", |
730 | "type-alarm": "Alarme", | 730 | "type-alarm": "Alarme", |
@@ -832,7 +832,7 @@ | @@ -832,7 +832,7 @@ | ||
832 | "delete-extension-text": "Attention, après la confirmation, l'extension et toutes les données associées deviendront irrécupérables.", | 832 | "delete-extension-text": "Attention, après la confirmation, l'extension et toutes les données associées deviendront irrécupérables.", |
833 | "delete-extension-title": "Êtes-vous sûr de vouloir supprimer l'extension '{{extensionId}}'?", | 833 | "delete-extension-title": "Êtes-vous sûr de vouloir supprimer l'extension '{{extensionId}}'?", |
834 | "delete-extensions-text": "Attention, après la confirmation, toutes les extensions sélectionnées seront supprimées.", | 834 | "delete-extensions-text": "Attention, après la confirmation, toutes les extensions sélectionnées seront supprimées.", |
835 | - "delete-extensions-title": "Êtes-vous sûr de vouloir supprimer {count, plural, 1 {1 extension} other {# extensions}}?", | 835 | + "delete-extensions-title": "Êtes-vous sûr de vouloir supprimer {count, plural, 1 {1 extension} other {# extensions} }?", |
836 | "device-name-expression": "expression du nom du dispositif", | 836 | "device-name-expression": "expression du nom du dispositif", |
837 | "device-name-filter": "Filtre de nom de dispositif", | 837 | "device-name-filter": "Filtre de nom de dispositif", |
838 | "device-type-expression": "expression de type de dispositif", | 838 | "device-type-expression": "expression de type de dispositif", |
@@ -918,7 +918,7 @@ | @@ -918,7 +918,7 @@ | ||
918 | "response-timeout": "Délai de réponse en millisecondes", | 918 | "response-timeout": "Délai de réponse en millisecondes", |
919 | "response-topic-expression": "Expression du topic de la réponse", | 919 | "response-topic-expression": "Expression du topic de la réponse", |
920 | "retry-interval": "Intervalle de nouvelle tentative en millisecondes", | 920 | "retry-interval": "Intervalle de nouvelle tentative en millisecondes", |
921 | - "selected-extensions": "{count, plural, 1 {1 extension} other {# extensions}} sélectionné", | 921 | + "selected-extensions": "{count, plural, 1 {1 extension} other {# extensions} } sélectionné", |
922 | "server-side-rpc": "RPC côté serveur", | 922 | "server-side-rpc": "RPC côté serveur", |
923 | "ssl": "Ssl", | 923 | "ssl": "Ssl", |
924 | "sync": { | 924 | "sync": { |
@@ -960,9 +960,9 @@ | @@ -960,9 +960,9 @@ | ||
960 | "delete-item-text": "Faites attention, après la confirmation, cet élément et toutes les données associées deviendront irrécupérables.", | 960 | "delete-item-text": "Faites attention, après la confirmation, cet élément et toutes les données associées deviendront irrécupérables.", |
961 | "delete-item-title": "Êtes-vous sûr de vouloir supprimer cet élément?", | 961 | "delete-item-title": "Êtes-vous sûr de vouloir supprimer cet élément?", |
962 | "delete-items": "Supprimer les éléments", | 962 | "delete-items": "Supprimer les éléments", |
963 | - "delete-items-action-title": "Supprimer {count, plural, 1 {1 élément} other {# éléments}}", | 963 | + "delete-items-action-title": "Supprimer {count, plural, 1 {1 élément} other {# éléments} }", |
964 | "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.", | 964 | "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.", |
965 | - "delete-items-title": "Etes-vous sûr de vouloir supprimer {count, plural, 1 {1 élément} other {# éléments}}?", | 965 | + "delete-items-title": "Etes-vous sûr de vouloir supprimer {count, plural, 1 {1 élément} other {# éléments} }?", |
966 | "item-details": "Détails de l'élément", | 966 | "item-details": "Détails de l'élément", |
967 | "no-items-text": "Aucun élément trouvé", | 967 | "no-items-text": "Aucun élément trouvé", |
968 | "scroll-to-top": "Défiler vers le haut" | 968 | "scroll-to-top": "Défiler vers le haut" |
@@ -1080,11 +1080,11 @@ | @@ -1080,11 +1080,11 @@ | ||
1080 | "delete-from-relation-text": "Attention, après la confirmation, l'entité actuelle ne sera pas liée à l'entité '{{entityName}}'.", | 1080 | "delete-from-relation-text": "Attention, après la confirmation, l'entité actuelle ne sera pas liée à l'entité '{{entityName}}'.", |
1081 | "delete-from-relation-title": "Etes-vous sûr de vouloir supprimer la relation de l'entité '{{entityName}}'?", | 1081 | "delete-from-relation-title": "Etes-vous sûr de vouloir supprimer la relation de l'entité '{{entityName}}'?", |
1082 | "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.", | 1082 | "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.", |
1083 | - "delete-from-relations-title": "Êtes-vous sûr de vouloir supprimer {count, plural, 1 {1 relation} other {# relations}}?", | 1083 | + "delete-from-relations-title": "Êtes-vous sûr de vouloir supprimer {count, plural, 1 {1 relation} other {# relations} }?", |
1084 | "delete-to-relation-text": "Attention, après la confirmation, l'entité '{{entityName}} ne sera plus liée à l'entité actuelle.", | 1084 | "delete-to-relation-text": "Attention, après la confirmation, l'entité '{{entityName}} ne sera plus liée à l'entité actuelle.", |
1085 | "delete-to-relation-title": "Êtes-vous sûr de vouloir supprimer la relation avec l'entité '{{entityName}}'?", | 1085 | "delete-to-relation-title": "Êtes-vous sûr de vouloir supprimer la relation avec l'entité '{{entityName}}'?", |
1086 | "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.", | 1086 | "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.", |
1087 | - "delete-to-relations-title": "Etes-vous sûr de vouloir supprimer {count, plural, 1 {1 relation} other {# relations}}?", | 1087 | + "delete-to-relations-title": "Etes-vous sûr de vouloir supprimer {count, plural, 1 {1 relation} other {# relations} }?", |
1088 | "direction": "Sens", | 1088 | "direction": "Sens", |
1089 | "direction-type": { | 1089 | "direction-type": { |
1090 | "FROM": "de", | 1090 | "FROM": "de", |
@@ -1105,7 +1105,7 @@ | @@ -1105,7 +1105,7 @@ | ||
1105 | "FROM": "De", | 1105 | "FROM": "De", |
1106 | "TO": "À" | 1106 | "TO": "À" |
1107 | }, | 1107 | }, |
1108 | - "selected-relations": "{count, plural, 1 {1 relation} other {# relations}} sélectionné", | 1108 | + "selected-relations": "{count, plural, 1 {1 relation} other {# relations} } sélectionné", |
1109 | "to-entity": "À l'entité", | 1109 | "to-entity": "À l'entité", |
1110 | "to-entity-name": "vers le nom de l'entité", | 1110 | "to-entity-name": "vers le nom de l'entité", |
1111 | "to-entity-type": "Vers le type d'entité", | 1111 | "to-entity-type": "Vers le type d'entité", |
@@ -1121,9 +1121,9 @@ | @@ -1121,9 +1121,9 @@ | ||
1121 | "delete": "Supprimer la chaîne de règles", | 1121 | "delete": "Supprimer la chaîne de règles", |
1122 | "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.", | 1122 | "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.", |
1123 | "delete-rulechain-title": "Voulez-vous vraiment supprimer la chaîne de règles '{{ruleChainName}}'?", | 1123 | "delete-rulechain-title": "Voulez-vous vraiment supprimer la chaîne de règles '{{ruleChainName}}'?", |
1124 | - "delete-rulechains-action-title": "Supprimer {count, plural, 1 {1 chaîne de règles} other {# chaînes de règles}}", | 1124 | + "delete-rulechains-action-title": "Supprimer {count, plural, 1 {1 chaîne de règles} other {# chaînes de règles} }", |
1125 | "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.", | 1125 | "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.", |
1126 | - "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}}?", | 1126 | + "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} }?", |
1127 | "description": "Description", | 1127 | "description": "Description", |
1128 | "details": "Détails", | 1128 | "details": "Détails", |
1129 | "events": "Evénements", | 1129 | "events": "Evénements", |
@@ -1220,9 +1220,9 @@ | @@ -1220,9 +1220,9 @@ | ||
1220 | "delete": "Supprimer le Tenant", | 1220 | "delete": "Supprimer le Tenant", |
1221 | "delete-tenant-text": "Attention, après la confirmation, le Tenant et toutes les données associées deviendront irrécupérables.", | 1221 | "delete-tenant-text": "Attention, après la confirmation, le Tenant et toutes les données associées deviendront irrécupérables.", |
1222 | "delete-tenant-title": "Etes-vous sûr de vouloir supprimer le tenant '{{tenantTitle}}'?", | 1222 | "delete-tenant-title": "Etes-vous sûr de vouloir supprimer le tenant '{{tenantTitle}}'?", |
1223 | - "delete-tenants-action-title": "Supprimer {count, plural, 1 {1 tenant} other {# tenants}}", | 1223 | + "delete-tenants-action-title": "Supprimer {count, plural, 1 {1 tenant} other {# tenants} }", |
1224 | "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.", | 1224 | "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.", |
1225 | - "delete-tenants-title": "Etes-vous sûr de vouloir supprimer {count, plural, 1 {1 tenant} other {# tenants}}?", | 1225 | + "delete-tenants-title": "Etes-vous sûr de vouloir supprimer {count, plural, 1 {1 tenant} other {# tenants} }?", |
1226 | "description": "Description", | 1226 | "description": "Description", |
1227 | "details": "Détails", | 1227 | "details": "Détails", |
1228 | "events": "Événements", | 1228 | "events": "Événements", |
@@ -1242,26 +1242,26 @@ | @@ -1242,26 +1242,26 @@ | ||
1242 | "timeinterval": { | 1242 | "timeinterval": { |
1243 | "advanced": "Avancé", | 1243 | "advanced": "Avancé", |
1244 | "days": "Jours", | 1244 | "days": "Jours", |
1245 | - "days-interval": "{days, plural, 1 {1 jour} other {# jours}}", | 1245 | + "days-interval": "{days, plural, 1 {1 jour} other {# jours} }", |
1246 | "hours": "Heures", | 1246 | "hours": "Heures", |
1247 | - "hours-interval": "{hours, plural, 1 {1 heure} other {# heures}}", | 1247 | + "hours-interval": "{hours, plural, 1 {1 heure} other {# heures} }", |
1248 | "minutes": "Minutes", | 1248 | "minutes": "Minutes", |
1249 | - "minutes-interval": "{minutes, plural, 1 {1 minute} other {# minutes}}", | 1249 | + "minutes-interval": "{minutes, plural, 1 {1 minute} other {# minutes} }", |
1250 | "seconds": "Secondes", | 1250 | "seconds": "Secondes", |
1251 | - "seconds-interval": "{seconds, plural, 1 {1 seconde} other {# secondes}}" | 1251 | + "seconds-interval": "{seconds, plural, 1 {1 seconde} other {# secondes} }" |
1252 | }, | 1252 | }, |
1253 | "timewindow": { | 1253 | "timewindow": { |
1254 | "date-range": "Plage de dates", | 1254 | "date-range": "Plage de dates", |
1255 | - "days": "{days, plural, 1 {jour} other {# jours}}", | 1255 | + "days": "{days, plural, 1 {jour} other {# jours} }", |
1256 | "edit": "Modifier timewindow", | 1256 | "edit": "Modifier timewindow", |
1257 | "history": "Historique", | 1257 | "history": "Historique", |
1258 | - "hours": "{hours, plural, 0 {heure} 1 {1 heure} other {# heures}}", | 1258 | + "hours": "{hours, plural, 0 {heure} 1 {1 heure} other {# heures} }", |
1259 | "last": "Dernier", | 1259 | "last": "Dernier", |
1260 | "last-prefix": "dernier", | 1260 | "last-prefix": "dernier", |
1261 | - "minutes": "{minutes, plural, 0 {minute} 1 {1 minute} other {# minutes}}", | 1261 | + "minutes": "{minutes, plural, 0 {minute} 1 {1 minute} other {# minutes} }", |
1262 | "period": "de {{startTime}} à {{endTime}}", | 1262 | "period": "de {{startTime}} à {{endTime}}", |
1263 | "realtime": "Temps réel", | 1263 | "realtime": "Temps réel", |
1264 | - "seconds": "{seconds, plural, 0 {second} 1 {1 second} other {# seconds}}", | 1264 | + "seconds": "{seconds, plural, 0 {second} 1 {1 second} other {# seconds} }", |
1265 | "time-period": "Période" | 1265 | "time-period": "Période" |
1266 | }, | 1266 | }, |
1267 | "user": { | 1267 | "user": { |
@@ -1281,9 +1281,9 @@ | @@ -1281,9 +1281,9 @@ | ||
1281 | "delete": "Supprimer l'utilisateur", | 1281 | "delete": "Supprimer l'utilisateur", |
1282 | "delete-user-text": "Attention, après la confirmation, l'utilisateur et toutes les données associées deviendront irrécupérables.", | 1282 | "delete-user-text": "Attention, après la confirmation, l'utilisateur et toutes les données associées deviendront irrécupérables.", |
1283 | "delete-user-title": "Etes-vous sûr de vouloir supprimer l'utilisateur '{{userEmail}}'?", | 1283 | "delete-user-title": "Etes-vous sûr de vouloir supprimer l'utilisateur '{{userEmail}}'?", |
1284 | - "delete-users-action-title": "Supprimer {count, plural, 1 {1 utilisateur} other {# utilisateurs}}", | 1284 | + "delete-users-action-title": "Supprimer {count, plural, 1 {1 utilisateur} other {# utilisateurs} }", |
1285 | "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.", | 1285 | "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.", |
1286 | - "delete-users-title": "Etes-vous sûr de vouloir supprimer {count, plural, 1 {1 utilisateur} other {# utilisateurs}}?", | 1286 | + "delete-users-title": "Etes-vous sûr de vouloir supprimer {count, plural, 1 {1 utilisateur} other {# utilisateurs} }?", |
1287 | "description": "Description", | 1287 | "description": "Description", |
1288 | "details": "Détails", | 1288 | "details": "Détails", |
1289 | "display-activation-link": "Afficher le lien d'activation", | 1289 | "display-activation-link": "Afficher le lien d'activation", |
@@ -1417,7 +1417,7 @@ | @@ -1417,7 +1417,7 @@ | ||
1417 | "general-settings": "Paramètres généraux", | 1417 | "general-settings": "Paramètres généraux", |
1418 | "height": "Hauteur", | 1418 | "height": "Hauteur", |
1419 | "margin": "Marge", | 1419 | "margin": "Marge", |
1420 | - "maximum-datasources": "Maximum {count, plural, 1 {1 datasource est autorisé.} other {# datasources sont autorisés}}", | 1420 | + "maximum-datasources": "Maximum {count, plural, 1 {1 datasource est autorisé.} other {# datasources sont autorisés} }", |
1421 | "mobile-mode-settings": "Paramètres du mode mobile", | 1421 | "mobile-mode-settings": "Paramètres du mode mobile", |
1422 | "order": "Ordre", | 1422 | "order": "Ordre", |
1423 | "padding": "Padding", | 1423 | "padding": "Padding", |
@@ -1511,9 +1511,9 @@ | @@ -1511,9 +1511,9 @@ | ||
1511 | "delete": "Supprimer le groupe de widgets", | 1511 | "delete": "Supprimer le groupe de widgets", |
1512 | "delete-widgets-bundle-text": "Attention, après la confirmation, le groupe de widgets et toutes les données associées deviendront irrécupérables.", | 1512 | "delete-widgets-bundle-text": "Attention, après la confirmation, le groupe de widgets et toutes les données associées deviendront irrécupérables.", |
1513 | "delete-widgets-bundle-title": "Êtes-vous sûr de vouloir supprimer le groupe de widgets '{{widgetsBundleTitle}}'?", | 1513 | "delete-widgets-bundle-title": "Êtes-vous sûr de vouloir supprimer le groupe de widgets '{{widgetsBundleTitle}}'?", |
1514 | - "delete-widgets-bundles-action-title": "Supprimer {count, plural, 1 {1 groupe de widgets} other {# groupes de widgets}}", | 1514 | + "delete-widgets-bundles-action-title": "Supprimer {count, plural, 1 {1 groupe de widgets} other {# groupes de widgets} }", |
1515 | "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.", | 1515 | "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.", |
1516 | - "delete-widgets-bundles-title": "Voulez-vous vraiment supprimer {count, plural, 1 {1 groupe de widgets} other {# groupes de widgets}}?", | 1516 | + "delete-widgets-bundles-title": "Voulez-vous vraiment supprimer {count, plural, 1 {1 groupe de widgets} other {# groupes de widgets} }?", |
1517 | "details": "Détails", | 1517 | "details": "Détails", |
1518 | "empty": "Le groupe de widgets est vide", | 1518 | "empty": "Le groupe de widgets est vide", |
1519 | "export": "Exporter le groupe de widgets", | 1519 | "export": "Exporter le groupe de widgets", |
@@ -626,7 +626,7 @@ | @@ -626,7 +626,7 @@ | ||
626 | "delete-device-title": "Вы точно хотите удалить устройство '{{deviceName}}'?", | 626 | "delete-device-title": "Вы точно хотите удалить устройство '{{deviceName}}'?", |
627 | "delete-device-text": "Внимание, после подтверждения устройство и все связанные с ним данные будут безвозвратно утеряны.", | 627 | "delete-device-text": "Внимание, после подтверждения устройство и все связанные с ним данные будут безвозвратно утеряны.", |
628 | "delete-devices-title": "Вы точно хотите удалить { count, plural, one {1 устройство} few {# устройства} other {# устройств} }?", | 628 | "delete-devices-title": "Вы точно хотите удалить { count, plural, one {1 устройство} few {# устройства} other {# устройств} }?", |
629 | - "delete-devices-action-title": "Удалить { count, plural, one {1 устройство} few {# устройства} other {# устройств} } }", | 629 | + "delete-devices-action-title": "Удалить { count, plural, one {1 устройство} few {# устройства} other {# устройств} }", |
630 | "delete-devices-text": "Внимание, после подтверждения выбранные устройства и все связанные с ними данные будут безвозвратно утеряны..", | 630 | "delete-devices-text": "Внимание, после подтверждения выбранные устройства и все связанные с ними данные будут безвозвратно утеряны..", |
631 | "unassign-device-title": "Вы точно хотите отозвать устройство '{{deviceName}}'?", | 631 | "unassign-device-title": "Вы точно хотите отозвать устройство '{{deviceName}}'?", |
632 | "unassign-device-text": "После подтверждения устройство будет недоступно клиенту.", | 632 | "unassign-device-text": "После подтверждения устройство будет недоступно клиенту.", |
@@ -1064,7 +1064,7 @@ | @@ -1064,7 +1064,7 @@ | ||
1064 | "delete-item-title": "Вы точно хотите удалить этот объект?", | 1064 | "delete-item-title": "Вы точно хотите удалить этот объект?", |
1065 | "delete-item-text": "Внимание, после подтверждения объект и все связанные с ним данные будут безвозвратно утеряны.", | 1065 | "delete-item-text": "Внимание, после подтверждения объект и все связанные с ним данные будут безвозвратно утеряны.", |
1066 | "delete-items-title": "Вы точно хотите удалить { count, plural, one {1 объект} few {# объекта} other {# объектов} }?", | 1066 | "delete-items-title": "Вы точно хотите удалить { count, plural, one {1 объект} few {# объекта} other {# объектов} }?", |
1067 | - "delete-items-action-title": "Удалить { count, plural, one {1 объект} few {# объекта} other {# объектов}", | 1067 | + "delete-items-action-title": "Удалить { count, plural, one {1 объект} few {# объекта} other {# объектов} }", |
1068 | "delete-items-text": "Внимание, после подтверждения выбранные объекты и все связанные с ними данные будут безвозвратно утеряны.", | 1068 | "delete-items-text": "Внимание, после подтверждения выбранные объекты и все связанные с ними данные будут безвозвратно утеряны.", |
1069 | "add-item-text": "Добавить новый объект", | 1069 | "add-item-text": "Добавить новый объект", |
1070 | "no-items-text": "Объекты не найдены", | 1070 | "no-items-text": "Объекты не найдены", |
@@ -1646,4 +1646,4 @@ | @@ -1646,4 +1646,4 @@ | ||
1646 | "cs_CZ": "Чешский" | 1646 | "cs_CZ": "Чешский" |
1647 | } | 1647 | } |
1648 | } | 1648 | } |
1649 | -} | ||
1649 | +} |
@@ -419,7 +419,7 @@ | @@ -419,7 +419,7 @@ | ||
419 | "assign-dashboards": "Kontrol panelleri ata", | 419 | "assign-dashboards": "Kontrol panelleri ata", |
420 | "assign-new-dashboard": "Yeni kontrol paneli ata", | 420 | "assign-new-dashboard": "Yeni kontrol paneli ata", |
421 | "assign-dashboards-text": "{ count, plural, 1 {1 kontrol panelini} other {# kontrol panelini} } kullanıcı grubuna ata", | 421 | "assign-dashboards-text": "{ count, plural, 1 {1 kontrol panelini} other {# kontrol panelini} } kullanıcı grubuna ata", |
422 | - "unassign-dashboards-action-text": "Müşterilerden atama { count, plural, 1 {1 gösterge tablosu} other {# panolar}}", | 422 | + "unassign-dashboards-action-text": "Müşterilerden atama { count, plural, 1 {1 gösterge tablosu} other {# panolar} }", |
423 | "delete-dashboards": "Kontrol panellerini sil", | 423 | "delete-dashboards": "Kontrol panellerini sil", |
424 | "unassign-dashboards": "Kontrol panellerinden atamayı kaldır", | 424 | "unassign-dashboards": "Kontrol panellerinden atamayı kaldır", |
425 | "unassign-dashboards-action-title": "{ count, plural, 1 {1 kontrol panelinin} other {# kontrol panelinin} } atamaları kullanıcı grubundan kaldır", | 425 | "unassign-dashboards-action-title": "{ count, plural, 1 {1 kontrol panelinin} other {# kontrol panelinin} } atamaları kullanıcı grubundan kaldır", |
@@ -710,7 +710,7 @@ | @@ -710,7 +710,7 @@ | ||
710 | "asset-name-starts-with": "İsmi '{{prefix}}' ile başlayan varlıklar", | 710 | "asset-name-starts-with": "İsmi '{{prefix}}' ile başlayan varlıklar", |
711 | "type-entity-view": "Varlık Görünümü", | 711 | "type-entity-view": "Varlık Görünümü", |
712 | "type-entity-views": "Varlık Görünümleri", | 712 | "type-entity-views": "Varlık Görünümleri", |
713 | - "list-of-entity-views": "{ count, plural, 1 {Bir varlık görünümü} other {# varlık görüntüleme}} listesi", | 713 | + "list-of-entity-views": "{ count, plural, 1 {Bir varlık görünümü} other {# varlık görüntüleme} } listesi", |
714 | "entity-view-name-starts-with": "Adı {{önek}} ile başlayan varlık görünümleri", | 714 | "entity-view-name-starts-with": "Adı {{önek}} ile başlayan varlık görünümleri", |
715 | "type-rule": "Kural", | 715 | "type-rule": "Kural", |
716 | "type-rules": "Kurallar", | 716 | "type-rules": "Kurallar", |
@@ -742,11 +742,11 @@ | @@ -742,11 +742,11 @@ | ||
742 | "alarm-name-starts-with": "İsmi '{{prefix}}' ile başlayan alarmlar", | 742 | "alarm-name-starts-with": "İsmi '{{prefix}}' ile başlayan alarmlar", |
743 | "type-rulechain": "Kural zinciri", | 743 | "type-rulechain": "Kural zinciri", |
744 | "type-rulechains": "Kural zincirleri", | 744 | "type-rulechains": "Kural zincirleri", |
745 | - "list-of-rulechains": "{ count, plural, 1 {Bir kural zinciri} other {# kural zincirinin listesi}}", | 745 | + "list-of-rulechains": "{ count, plural, 1 {Bir kural zinciri} other {# kural zincirinin listesi} }", |
746 | "rulechain-name-starts-with": "İsimleri {{prefix}} ile başlayan kural zincirleri", | 746 | "rulechain-name-starts-with": "İsimleri {{prefix}} ile başlayan kural zincirleri", |
747 | "type-rulenode": "Kural düğümü", | 747 | "type-rulenode": "Kural düğümü", |
748 | "type-rulenodes": "Kural düğümleri", | 748 | "type-rulenodes": "Kural düğümleri", |
749 | - "list-of-rulenodes": "{ count, plural, 1 {Bir kural node} other {# kural düğümünün listesi}}", | 749 | + "list-of-rulenodes": "{ count, plural, 1 {Bir kural node} other {# kural düğümünün listesi} }", |
750 | "rulenode-name-starts-with": "İsimleri '{{prefix}} ile başlayan kural düğümleri", | 750 | "rulenode-name-starts-with": "İsimleri '{{prefix}} ile başlayan kural düğümleri", |
751 | "type-current-customer": "Mevcut Müşteri", | 751 | "type-current-customer": "Mevcut Müşteri", |
752 | "search": "Öğeleri ara", | 752 | "search": "Öğeleri ara", |
@@ -765,7 +765,7 @@ | @@ -765,7 +765,7 @@ | ||
765 | "aliases": "Varlık Görünümü takma adları", | 765 | "aliases": "Varlık Görünümü takma adları", |
766 | "no-alias-matching": "'{{alias}} bulunamadı. ", | 766 | "no-alias-matching": "'{{alias}} bulunamadı. ", |
767 | "no-aliases-found": "Takma ad bulunamadı", | 767 | "no-aliases-found": "Takma ad bulunamadı", |
768 | - "no-key-matching": "'{{anahtar bulunamadı.", | 768 | + "no-key-matching": "'{{key}}' bulunamadı.", |
769 | "no-keys-found": "Anahtar bulunamadı.", | 769 | "no-keys-found": "Anahtar bulunamadı.", |
770 | "create-new-alias": "Yeni bir tane oluştur!", | 770 | "create-new-alias": "Yeni bir tane oluştur!", |
771 | "create-new-key": "Yeni bir tane oluştur!", | 771 | "create-new-key": "Yeni bir tane oluştur!", |
@@ -792,21 +792,21 @@ | @@ -792,21 +792,21 @@ | ||
792 | "add-entity-view-text": "Yeni varlık görünümü ekle", | 792 | "add-entity-view-text": "Yeni varlık görünümü ekle", |
793 | "delete": "Varlık görünümünü sil", | 793 | "delete": "Varlık görünümünü sil", |
794 | "assign-entity-views": "Varlık görünümleri atama", | 794 | "assign-entity-views": "Varlık görünümleri atama", |
795 | - "assign-entity-views-text": "Müşteriye { count, plural, 1 {1 entityView} other {# entityViews}} atayın ", | 795 | + "assign-entity-views-text": "Müşteriye { count, plural, 1 {1 entityView} other {# entityViews} } atayın ", |
796 | "delete-entity-views": "Varlık görünümlerini sil", | 796 | "delete-entity-views": "Varlık görünümlerini sil", |
797 | "unassign-from-customer": "Müşteriden atama", | 797 | "unassign-from-customer": "Müşteriden atama", |
798 | "unassign-entity-views": "Varlık görünümlerini atama", | 798 | "unassign-entity-views": "Varlık görünümlerini atama", |
799 | - "unassign-entity-views-action-title": "Müşteriden atama { count, plural, 1 {1 entityView} other {# entityViews}}", | 799 | + "unassign-entity-views-action-title": "Müşteriden atama { count, plural, 1 {1 entityView} other {# entityViews} }", |
800 | "assign-new-entity-view": "Yeni varlık görünümü atama", | 800 | "assign-new-entity-view": "Yeni varlık görünümü atama", |
801 | "delete-entity-view-title": "Varlık görünümünü silmek istediğinizden emin misiniz?, {{entityViewName}} '? ", | 801 | "delete-entity-view-title": "Varlık görünümünü silmek istediğinizden emin misiniz?, {{entityViewName}} '? ", |
802 | "delete-entity-view-text": "Dikkatli olun, onaylandıktan sonra varlık görünümü ve ilgili tüm veriler kurtarılamayacak.", | 802 | "delete-entity-view-text": "Dikkatli olun, onaylandıktan sonra varlık görünümü ve ilgili tüm veriler kurtarılamayacak.", |
803 | - "delete-entity-views-title": "{ count, plural, 1 {1 entityView} other {# entityViews}} varlık görünümüne sahip olmak istediğinizden emin misiniz?", | ||
804 | - "delete-entity-views-action-title": "Sil { count, plural, 1 {1 entityView} other {# entityViews}}", | 803 | + "delete-entity-views-title": "{ count, plural, 1 {1 entityView} other {# entityViews} } varlık görünümüne sahip olmak istediğinizden emin misiniz?", |
804 | + "delete-entity-views-action-title": "Sil { count, plural, 1 {1 entityView} other {# entityViews} }", | ||
805 | "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.", | 805 | "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.", |
806 | "unassign-entity-view-title": "Varlık görünümünün atamasını kaldırmak istediğinizden emin misiniz? {{entityViewName}} '? ", | 806 | "unassign-entity-view-title": "Varlık görünümünün atamasını kaldırmak istediğinizden emin misiniz? {{entityViewName}} '? ", |
807 | "unassign-entity-view-text": "Onaydan sonra varlık görünümü atanmamış olacak ve müşteri tarafından erişilemeyecektir.", | 807 | "unassign-entity-view-text": "Onaydan sonra varlık görünümü atanmamış olacak ve müşteri tarafından erişilemeyecektir.", |
808 | "unassign-entity-view": "Varlık görünümünün atamasını kaldır", | 808 | "unassign-entity-view": "Varlık görünümünün atamasını kaldır", |
809 | - "unassign-entity-views-title": "{ count, plural, 1 {1 entityView} other {# entityViews}} hesabının atamasını kaldırmak istediğinizden emin misiniz?", | 809 | + "unassign-entity-views-title": "{ count, plural, 1 {1 entityView} other {# entityViews} } hesabının atamasını kaldırmak istediğinizden emin misiniz?", |
810 | "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.", | 810 | "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.", |
811 | "entity-view-type": "Varlık Görünümü türü", | 811 | "entity-view-type": "Varlık Görünümü türü", |
812 | "entity-view-type-required": "Varlık Görünümü türü gerekli.", | 812 | "entity-view-type-required": "Varlık Görünümü türü gerekli.", |
@@ -861,7 +861,7 @@ | @@ -861,7 +861,7 @@ | ||
861 | }, | 861 | }, |
862 | "extension": { | 862 | "extension": { |
863 | "extensions": "Uzantılar", | 863 | "extensions": "Uzantılar", |
864 | - "selected-extensions": "{ count, plural, 1 {1 uzantı} other {# extensions}} seçildi", | 864 | + "selected-extensions": "{ count, plural, 1 {1 uzantı} other {# extensions} } seçildi", |
865 | "type": "Tür", | 865 | "type": "Tür", |
866 | "key": "Anahtar", | 866 | "key": "Anahtar", |
867 | "value": "Değer", | 867 | "value": "Değer", |
@@ -875,7 +875,7 @@ | @@ -875,7 +875,7 @@ | ||
875 | "edit": "Uzantıyı düzenle", | 875 | "edit": "Uzantıyı düzenle", |
876 | "delete-extension-title": "{{ExtensionId}} uzantısını silmek istediğinizden emin misiniz? ", | 876 | "delete-extension-title": "{{ExtensionId}} uzantısını silmek istediğinizden emin misiniz? ", |
877 | "delete-extension-text": "Dikkatli olun, onaylamadan sonra uzantı ve ilgili tüm veriler kurtarılamaz.", | 877 | "delete-extension-text": "Dikkatli olun, onaylamadan sonra uzantı ve ilgili tüm veriler kurtarılamaz.", |
878 | - "delete-extensions-title": "{ count, plural, 1 {1 uzantı} other {# extensions}} silmek istediğinizden emin misiniz?", | 878 | + "delete-extensions-title": "{ count, plural, 1 {1 uzantı} other {# extensions} } silmek istediğinizden emin misiniz?", |
879 | "delete-extensions-text": "Dikkatli olun, onaylandıktan sonra tüm seçilen uzantılar kaldırılacak.", | 879 | "delete-extensions-text": "Dikkatli olun, onaylandıktan sonra tüm seçilen uzantılar kaldırılacak.", |
880 | "converters": "Dönüştürücü", | 880 | "converters": "Dönüştürücü", |
881 | "converter-id": "Dönüştürücü kimliği", | 881 | "converter-id": "Dönüştürücü kimliği", |
@@ -1606,4 +1606,4 @@ | @@ -1606,4 +1606,4 @@ | ||
1606 | "cs_CZ": "Çekçe" | 1606 | "cs_CZ": "Çekçe" |
1607 | } | 1607 | } |
1608 | } | 1608 | } |
1609 | -} | ||
1609 | +} |
@@ -457,8 +457,8 @@ | @@ -457,8 +457,8 @@ | ||
457 | "customer-details": "Інформація про клієнта", | 457 | "customer-details": "Інформація про клієнта", |
458 | "delete-customer-title": "Ви впевнені, що хочете видалити клієнта '{{customerTitle}}'?", | 458 | "delete-customer-title": "Ви впевнені, що хочете видалити клієнта '{{customerTitle}}'?", |
459 | "delete-customer-text": "Будьте обережні, після підтвердження, клієнт та всі пов'язані з ним дані, стануть недоступними.", | 459 | "delete-customer-text": "Будьте обережні, після підтвердження, клієнт та всі пов'язані з ним дані, стануть недоступними.", |
460 | - "delete-customers-title": "Ви впевнені, що хочете видалити {count, plural, 1 {1 клієнт}, інші {# клієнти}}?", | ||
461 | - "delete-customers-action-title": "Видалити{ count, plural, 1 {1 клієнт} other {# клієнти} }", | 460 | + "delete-customers-title": "Ви впевнені, що хочете видалити { count, plural, 1 {1 клієнт} other {# клієнти} }?", |
461 | + "delete-customers-action-title": "Видалити { count, plural, 1 {1 клієнт} other {# клієнти} }", | ||
462 | "delete-customers-text": "Будьте обережні, після підтвердження, всі вибрані клієнти будуть видалені і всі пов'язані з ними дані, стануть недоступними.", | 462 | "delete-customers-text": "Будьте обережні, після підтвердження, всі вибрані клієнти будуть видалені і всі пов'язані з ними дані, стануть недоступними.", |
463 | "manage-users": "Керування користувачами", | 463 | "manage-users": "Керування користувачами", |
464 | "manage-assets": "Керування активами", | 464 | "manage-assets": "Керування активами", |
@@ -474,7 +474,7 @@ | @@ -474,7 +474,7 @@ | ||
474 | "select-customer": "Виберіть клієнта", | 474 | "select-customer": "Виберіть клієнта", |
475 | "no-customers-matching": "Клієнтів, які відповідають '{{entity}}' не знайдено.", | 475 | "no-customers-matching": "Клієнтів, які відповідають '{{entity}}' не знайдено.", |
476 | "customer-required": "Необхідно задати клієнта", | 476 | "customer-required": "Необхідно задати клієнта", |
477 | - "selected-customers": "{ count, plural, 1 {1 клієнт} інші {# клієнти} } вибрано", | 477 | + "selected-customers": "{ count, plural, 1 {1 клієнт} other {# клієнти} } вибрано", |
478 | "search": "Пошук клієнтів", | 478 | "search": "Пошук клієнтів", |
479 | "select-group-to-add": "Виберіть цільову групу, щоб додати вибраних клієнтів", | 479 | "select-group-to-add": "Виберіть цільову групу, щоб додати вибраних клієнтів", |
480 | "select-group-to-move": "Виберіть цільову групу для переміщення вибраних клієнтів", | 480 | "select-group-to-move": "Виберіть цільову групу для переміщення вибраних клієнтів", |
@@ -596,11 +596,11 @@ | @@ -596,11 +596,11 @@ | ||
596 | "manage-credentials": "管理凭据", | 596 | "manage-credentials": "管理凭据", |
597 | "delete": "删除设备", | 597 | "delete": "删除设备", |
598 | "assign-devices": "分配设备", | 598 | "assign-devices": "分配设备", |
599 | - "assign-devices-text": "将{count,plural,1 {1 设备} other {# 设备}}分配给客户", | 599 | + "assign-devices-text": "将{count,plural,1 {1 设备} other {# 设备} }分配给客户", |
600 | "delete-devices": "删除设备", | 600 | "delete-devices": "删除设备", |
601 | "unassign-from-customer": "取消分配客户", | 601 | "unassign-from-customer": "取消分配客户", |
602 | "unassign-devices": "取消分配设备", | 602 | "unassign-devices": "取消分配设备", |
603 | - "unassign-devices-action-title": "从客户处取消分配{count,plural,1 {1 设备} other {# 设备}}", | 603 | + "unassign-devices-action-title": "从客户处取消分配{count,plural,1 {1 设备} other {# 设备} }", |
604 | "assign-new-device": "分配新设备", | 604 | "assign-new-device": "分配新设备", |
605 | "make-public-device-title": "您确定要将设备 '{{deviceName}}' 设为公开吗?", | 605 | "make-public-device-title": "您确定要将设备 '{{deviceName}}' 设为公开吗?", |
606 | "make-public-device-text": "确认后,设备及其所有数据将被设为公开并可被其他人访问。", | 606 | "make-public-device-text": "确认后,设备及其所有数据将被设为公开并可被其他人访问。", |
@@ -609,13 +609,13 @@ | @@ -609,13 +609,13 @@ | ||
609 | "view-credentials": "查看凭据", | 609 | "view-credentials": "查看凭据", |
610 | "delete-device-title": "您确定要删除设备的{{deviceName}}吗?", | 610 | "delete-device-title": "您确定要删除设备的{{deviceName}}吗?", |
611 | "delete-device-text": "小心!确认后设备及其所有相关数据将不可恢复。", | 611 | "delete-device-text": "小心!确认后设备及其所有相关数据将不可恢复。", |
612 | - "delete-devices-title": "您确定要删除{count,plural,1 {1 设备} other {# 设备}} 吗?", | ||
613 | - "delete-devices-action-title": "删除 {count,plural,1 {1 设备} other {# 设备}}", | 612 | + "delete-devices-title": "您确定要删除{count,plural,1 {1 设备} other {# 设备} } 吗?", |
613 | + "delete-devices-action-title": "删除 {count,plural,1 {1 设备} other {# 设备} }", | ||
614 | "delete-devices-text": "小心!确认后所有选定的设备将被删除,所有相关数据将不可恢复。", | 614 | "delete-devices-text": "小心!确认后所有选定的设备将被删除,所有相关数据将不可恢复。", |
615 | "unassign-device-title": "您确定要取消分配设备 '{{deviceName}}'?", | 615 | "unassign-device-title": "您确定要取消分配设备 '{{deviceName}}'?", |
616 | "unassign-device-text": "确认后,设备将被取消分配,客户将无法访问。", | 616 | "unassign-device-text": "确认后,设备将被取消分配,客户将无法访问。", |
617 | "unassign-device": "取消分配设备", | 617 | "unassign-device": "取消分配设备", |
618 | - "unassign-devices-title": "您确定要取消分配{count,plural,1 {1 设备} other {# 设备}} 吗?", | 618 | + "unassign-devices-title": "您确定要取消分配{count,plural,1 {1 设备} other {# 设备} } 吗?", |
619 | "unassign-devices-text": "确认后,所有选定的设备将被取消分配,并且客户将无法访问。", | 619 | "unassign-devices-text": "确认后,所有选定的设备将被取消分配,并且客户将无法访问。", |
620 | "device-credentials": "设备凭据", | 620 | "device-credentials": "设备凭据", |
621 | "credentials-type": "凭据类型", | 621 | "credentials-type": "凭据类型", |
@@ -792,7 +792,7 @@ | @@ -792,7 +792,7 @@ | ||
792 | "delete-entity-views": "删除实体视图", | 792 | "delete-entity-views": "删除实体视图", |
793 | "unassign-from-customer": "取消分配客户", | 793 | "unassign-from-customer": "取消分配客户", |
794 | "unassign-entity-views": "取消分配实体视图", | 794 | "unassign-entity-views": "取消分配实体视图", |
795 | - "unassign-entity-views-action-title": "从客户处取消分配{count,plural,1 {1 实体视图} other {# 实体视图}}", | 795 | + "unassign-entity-views-action-title": "从客户处取消分配{count,plural,1 {1 实体视图} other {# 实体视图} }", |
796 | "assign-new-entity-view": "分配新实体视图", | 796 | "assign-new-entity-view": "分配新实体视图", |
797 | "delete-entity-view-title": "确定要删除实体视图 '{{entityViewName}}'?", | 797 | "delete-entity-view-title": "确定要删除实体视图 '{{entityViewName}}'?", |
798 | "delete-entity-view-text": "小心!确认后实体视图及其所有相关数据将不可恢复。", | 798 | "delete-entity-view-text": "小心!确认后实体视图及其所有相关数据将不可恢复。", |
@@ -1192,7 +1192,7 @@ | @@ -1192,7 +1192,7 @@ | ||
1192 | "set-root-rulechain-text": "确认之后,规则链将变为根规格链,并将处理所有传入的传输消息。", | 1192 | "set-root-rulechain-text": "确认之后,规则链将变为根规格链,并将处理所有传入的传输消息。", |
1193 | "delete-rulechain-title": " 确实要删除规则链'{{ruleChainName}}'吗?", | 1193 | "delete-rulechain-title": " 确实要删除规则链'{{ruleChainName}}'吗?", |
1194 | "delete-rulechain-text": "小心,在确认规则链和所有相关数据将变得不可恢复。", | 1194 | "delete-rulechain-text": "小心,在确认规则链和所有相关数据将变得不可恢复。", |
1195 | - "delete-rulechains-title": "确实要删除{count, plural, 1 { 1 规则链}其他{# 规则链库}}吗?", | 1195 | + "delete-rulechains-title": "确实要删除{count, plural, 1 { 1 规则链} other {# 规则链库} }吗?", |
1196 | "delete-rulechains-action-title": "删除 { count, plural, 1 {1 规则链} other {# 规则链库} }", | 1196 | "delete-rulechains-action-title": "删除 { count, plural, 1 {1 规则链} other {# 规则链库} }", |
1197 | "delete-rulechains-text": "小心,确认后,所有选定的规则链将被删除,所有相关的数据将变得不可恢复。", | 1197 | "delete-rulechains-text": "小心,确认后,所有选定的规则链将被删除,所有相关的数据将变得不可恢复。", |
1198 | "add-rulechain-text": "添加新的规则链", | 1198 | "add-rulechain-text": "添加新的规则链", |
@@ -1283,7 +1283,7 @@ | @@ -1283,7 +1283,7 @@ | ||
1283 | "tenant-details": "租客详情", | 1283 | "tenant-details": "租客详情", |
1284 | "delete-tenant-title": "您确定要删除租户'{{tenantTitle}}'吗?", | 1284 | "delete-tenant-title": "您确定要删除租户'{{tenantTitle}}'吗?", |
1285 | "delete-tenant-text": "小心!确认后,租户和所有相关数据将不可恢复。", | 1285 | "delete-tenant-text": "小心!确认后,租户和所有相关数据将不可恢复。", |
1286 | - "delete-tenants-title": "您确定要删除 {count,plural,1 {1 租户} other {# 租户}} 吗?", | 1286 | + "delete-tenants-title": "您确定要删除 {count,plural,1 {1 租户} other {# 租户} } 吗?", |
1287 | "delete-tenants-action-title": "删除 { count, plural, 1 {1 租户} other {# 租户} }", | 1287 | "delete-tenants-action-title": "删除 { count, plural, 1 {1 租户} other {# 租户} }", |
1288 | "delete-tenants-text": "小心!确认后,所有选定的租户将被删除,所有相关数据将不可恢复。", | 1288 | "delete-tenants-text": "小心!确认后,所有选定的租户将被删除,所有相关数据将不可恢复。", |
1289 | "title": "标题", | 1289 | "title": "标题", |