Showing
62 changed files
with
4635 additions
and
12 deletions
Too many changes to show.
To preserve performance only 62 of 93 files are displayed.
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 | +} |