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 | 320 | type: 'link', |
321 | 321 | path: '/home', |
322 | 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 | 349 | return sections; |
327 | 350 | } |
328 | 351 | |
329 | 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 | 395 | return homeSections; |
333 | 396 | } |
334 | 397 | ... | ... |
... | ... | @@ -56,8 +56,14 @@ export class TranslateDefaultCompiler extends TranslateMessageFormatCompiler { |
56 | 56 | } |
57 | 57 | |
58 | 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 | 67 | const res = tokens.filter( |
62 | 68 | (value) => typeof value !== 'string' && value.type === 'plural' |
63 | 69 | ); | ... | ... |
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 | 16 | |
17 | 17 | import { NgModule } from '@angular/core'; |
18 | 18 | |
19 | -// import { AdminModule } from './admin/admin.module'; | |
19 | +import { AdminModule } from './admin/admin.module'; | |
20 | 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 | 23 | // import { CustomerModule } from '@modules/home/pages/customer/customer.module'; |
23 | 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 | 27 | @NgModule({ |
27 | 28 | exports: [ |
28 | -// AdminModule, | |
29 | + AdminModule, | |
29 | 30 | HomeLinksModule, |
30 | -// ProfileModule, | |
31 | + ProfileModule, | |
32 | + TenantModule, | |
31 | 33 | // CustomerModule, |
32 | 34 | // AuditLogModule, |
33 | -// UserModule | |
35 | + UserModule | |
34 | 36 | ] |
35 | 37 | }) |
36 | 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 | +} | ... | ... |