Showing
93 changed files
with
6717 additions
and
145 deletions
ui-ngx/src/app/core/http/admin.service.ts
0 → 100644
1 | +/// | |
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | |
3 | +/// | |
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | +/// you may not use this file except in compliance with the License. | |
6 | +/// You may obtain a copy of the License at | |
7 | +/// | |
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | |
9 | +/// | |
10 | +/// Unless required by applicable law or agreed to in writing, software | |
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | |
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | +/// See the License for the specific language governing permissions and | |
14 | +/// limitations under the License. | |
15 | +/// | |
16 | + | |
17 | +import { Injectable } from '@angular/core'; | |
18 | +import { defaultHttpOptions } from './http-utils'; | |
19 | +import { Observable } from 'rxjs/index'; | |
20 | +import { HttpClient } from '@angular/common/http'; | |
21 | +import {AdminSettings, MailServerSettings, SecuritySettings} from '@shared/models/settings.models'; | |
22 | + | |
23 | +@Injectable({ | |
24 | + providedIn: 'root' | |
25 | +}) | |
26 | +export class AdminService { | |
27 | + | |
28 | + constructor( | |
29 | + private http: HttpClient | |
30 | + ) { } | |
31 | + | |
32 | + public getAdminSettings<T>(key: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<AdminSettings<T>> { | |
33 | + return this.http.get<AdminSettings<T>>(`/api/admin/settings/${key}`, defaultHttpOptions(ignoreLoading, ignoreErrors)); | |
34 | + } | |
35 | + | |
36 | + public saveAdminSettings<T>(adminSettings: AdminSettings<T>, | |
37 | + ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<AdminSettings<T>> { | |
38 | + return this.http.post<AdminSettings<T>>('/api/admin/settings', adminSettings, defaultHttpOptions(ignoreLoading, ignoreErrors)); | |
39 | + } | |
40 | + | |
41 | + public sendTestMail(adminSettings: AdminSettings<MailServerSettings>, | |
42 | + ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<void> { | |
43 | + return this.http.post<void>('/api/admin/settings/testMail', adminSettings, defaultHttpOptions(ignoreLoading, ignoreErrors)); | |
44 | + } | |
45 | + | |
46 | + public getSecuritySettings(ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<SecuritySettings> { | |
47 | + return this.http.get<SecuritySettings>(`/api/admin/securitySettings`, defaultHttpOptions(ignoreLoading, ignoreErrors)); | |
48 | + } | |
49 | + | |
50 | + public saveSecuritySettings(securitySettings: SecuritySettings, | |
51 | + ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<SecuritySettings> { | |
52 | + return this.http.post<SecuritySettings>('/api/admin/securitySettings', securitySettings, | |
53 | + defaultHttpOptions(ignoreLoading, ignoreErrors)); | |
54 | + } | |
55 | +} | ... | ... |
ui-ngx/src/app/core/http/customer.service.ts
0 → 100644
1 | +/// | |
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | |
3 | +/// | |
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | +/// you may not use this file except in compliance with the License. | |
6 | +/// You may obtain a copy of the License at | |
7 | +/// | |
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | |
9 | +/// | |
10 | +/// Unless required by applicable law or agreed to in writing, software | |
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | |
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | +/// See the License for the specific language governing permissions and | |
14 | +/// limitations under the License. | |
15 | +/// | |
16 | + | |
17 | +import { Injectable } from '@angular/core'; | |
18 | +import { defaultHttpOptions } from './http-utils'; | |
19 | +import { Observable } from 'rxjs/index'; | |
20 | +import { HttpClient } from '@angular/common/http'; | |
21 | +import { PageLink } from '@shared/models/page/page-link'; | |
22 | +import { PageData } from '@shared/models/page/page-data'; | |
23 | +import { Customer } from '@shared/models/customer.model'; | |
24 | + | |
25 | +@Injectable({ | |
26 | + providedIn: 'root' | |
27 | +}) | |
28 | +export class CustomerService { | |
29 | + | |
30 | + constructor( | |
31 | + private http: HttpClient | |
32 | + ) { } | |
33 | + | |
34 | + public getCustomers(tenantId: string, pageLink: PageLink, ignoreErrors: boolean = false, | |
35 | + ignoreLoading: boolean = false): Observable<PageData<Customer>> { | |
36 | + return this.http.get<PageData<Customer>>(`/api/tenant/${tenantId}/customers${pageLink.toQuery()}`, | |
37 | + defaultHttpOptions(ignoreLoading, ignoreErrors)); | |
38 | + } | |
39 | + | |
40 | + public getCustomer(customerId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Customer> { | |
41 | + return this.http.get<Customer>(`/api/customer/${customerId}`, defaultHttpOptions(ignoreLoading, ignoreErrors)); | |
42 | + } | |
43 | + | |
44 | + public saveCustomer(customer: Customer, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Customer> { | |
45 | + return this.http.post<Customer>('/api/customer', customer, defaultHttpOptions(ignoreLoading, ignoreErrors)); | |
46 | + } | |
47 | + | |
48 | + public deleteCustomer(customerId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false) { | |
49 | + return this.http.delete(`/api/customer/${customerId}`, defaultHttpOptions(ignoreLoading, ignoreErrors)); | |
50 | + } | |
51 | + | |
52 | +} | ... | ... |
1 | +/// | |
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | |
3 | +/// | |
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | +/// you may not use this file except in compliance with the License. | |
6 | +/// You may obtain a copy of the License at | |
7 | +/// | |
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | |
9 | +/// | |
10 | +/// Unless required by applicable law or agreed to in writing, software | |
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | |
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | +/// See the License for the specific language governing permissions and | |
14 | +/// limitations under the License. | |
15 | +/// | |
16 | + | |
17 | +import { Injectable } from '@angular/core'; | |
18 | +import { defaultHttpOptions } from './http-utils'; | |
19 | +import { Observable } from 'rxjs/index'; | |
20 | +import { HttpClient } from '@angular/common/http'; | |
21 | +import { PageLink } from '@shared/models/page/page-link'; | |
22 | +import { PageData } from '@shared/models/page/page-data'; | |
23 | +import { Tenant } from '@shared/models/tenant.model'; | |
24 | +import {DashboardInfo, Dashboard} from '@shared/models/dashboard.models'; | |
25 | +import {map} from 'rxjs/operators'; | |
26 | + | |
27 | +@Injectable({ | |
28 | + providedIn: 'root' | |
29 | +}) | |
30 | +export class DashboardService { | |
31 | + | |
32 | + constructor( | |
33 | + private http: HttpClient | |
34 | + ) { } | |
35 | + | |
36 | + public getTenantDashboards(pageLink: PageLink, ignoreErrors: boolean = false, | |
37 | + ignoreLoading: boolean = false): Observable<PageData<DashboardInfo>> { | |
38 | + return this.http.get<PageData<DashboardInfo>>(`/api/tenant/dashboards${pageLink.toQuery()}`, | |
39 | + defaultHttpOptions(ignoreLoading, ignoreErrors)); | |
40 | + } | |
41 | + | |
42 | + public getTenantDashboardsByTenantId(tenantId: string, pageLink: PageLink, ignoreErrors: boolean = false, | |
43 | + ignoreLoading: boolean = false): Observable<PageData<DashboardInfo>> { | |
44 | + return this.http.get<PageData<DashboardInfo>>(`/api/tenant/${tenantId}/dashboards${pageLink.toQuery()}`, | |
45 | + defaultHttpOptions(ignoreLoading, ignoreErrors)); | |
46 | + } | |
47 | + | |
48 | + public getCustomerDashboards(customerId: string, pageLink: PageLink, ignoreErrors: boolean = false, | |
49 | + ignoreLoading: boolean = false): Observable<PageData<DashboardInfo>> { | |
50 | + return this.http.get<PageData<DashboardInfo>>(`/api/customer/${customerId}/dashboards${pageLink.toQuery()}`, | |
51 | + defaultHttpOptions(ignoreLoading, ignoreErrors)).pipe( | |
52 | + map( dashboards => { | |
53 | + dashboards.data = dashboards.data.filter(dashboard => { | |
54 | + return dashboard.title.toUpperCase().includes(pageLink.textSearch.toUpperCase()); | |
55 | + }); | |
56 | + return dashboards; | |
57 | + } | |
58 | + )); | |
59 | + } | |
60 | + | |
61 | + public getDashboard(dashboardId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Dashboard> { | |
62 | + return this.http.get<Dashboard>(`/api/dashboard/${dashboardId}`, defaultHttpOptions(ignoreLoading, ignoreErrors)); | |
63 | + } | |
64 | + | |
65 | + public getDashboardInfo(dashboardId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<DashboardInfo> { | |
66 | + return this.http.get<DashboardInfo>(`/api/dashboard/info/${dashboardId}`, defaultHttpOptions(ignoreLoading, ignoreErrors)); | |
67 | + } | |
68 | + | |
69 | + public saveDashboard(dashboard: Dashboard, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Dashboard> { | |
70 | + return this.http.post<Dashboard>('/api/dashboard', dashboard, defaultHttpOptions(ignoreLoading, ignoreErrors)); | |
71 | + } | |
72 | + | |
73 | + public deleteDashboard(dashboardId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false) { | |
74 | + return this.http.delete(`/api/dashboard/${dashboardId}`, defaultHttpOptions(ignoreLoading, ignoreErrors)); | |
75 | + } | |
76 | + | |
77 | +} | ... | ... |
ui-ngx/src/app/core/http/tenant.service.ts
0 → 100644
1 | +/// | |
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | |
3 | +/// | |
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | +/// you may not use this file except in compliance with the License. | |
6 | +/// You may obtain a copy of the License at | |
7 | +/// | |
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | |
9 | +/// | |
10 | +/// Unless required by applicable law or agreed to in writing, software | |
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | |
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | +/// See the License for the specific language governing permissions and | |
14 | +/// limitations under the License. | |
15 | +/// | |
16 | + | |
17 | +import { Injectable } from '@angular/core'; | |
18 | +import { defaultHttpOptions } from './http-utils'; | |
19 | +import { Observable } from 'rxjs/index'; | |
20 | +import { HttpClient } from '@angular/common/http'; | |
21 | +import { PageLink } from '@shared/models/page/page-link'; | |
22 | +import { PageData } from '@shared/models/page/page-data'; | |
23 | +import { Tenant } from '@shared/models/tenant.model'; | |
24 | + | |
25 | +@Injectable({ | |
26 | + providedIn: 'root' | |
27 | +}) | |
28 | +export class TenantService { | |
29 | + | |
30 | + constructor( | |
31 | + private http: HttpClient | |
32 | + ) { } | |
33 | + | |
34 | + public getTenants(pageLink: PageLink, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<PageData<Tenant>> { | |
35 | + return this.http.get<PageData<Tenant>>(`/api/tenants${pageLink.toQuery()}`, defaultHttpOptions(ignoreLoading, ignoreErrors)); | |
36 | + } | |
37 | + | |
38 | + public getTenant(tenantId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Tenant> { | |
39 | + return this.http.get<Tenant>(`/api/tenant/${tenantId}`, defaultHttpOptions(ignoreLoading, ignoreErrors)); | |
40 | + } | |
41 | + | |
42 | + public saveTenant(tenant: Tenant, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Tenant> { | |
43 | + return this.http.post<Tenant>('/api/tenant', tenant, defaultHttpOptions(ignoreLoading, ignoreErrors)); | |
44 | + } | |
45 | + | |
46 | + public deleteTenant(tenantId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false) { | |
47 | + return this.http.delete(`/api/tenant/${tenantId}`, defaultHttpOptions(ignoreLoading, ignoreErrors)); | |
48 | + } | |
49 | + | |
50 | +} | ... | ... |
... | ... | @@ -320,15 +320,78 @@ export class MenuService { |
320 | 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 | +} | ... | ... |
1 | +/// | |
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | |
3 | +/// | |
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | +/// you may not use this file except in compliance with the License. | |
6 | +/// You may obtain a copy of the License at | |
7 | +/// | |
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | |
9 | +/// | |
10 | +/// Unless required by applicable law or agreed to in writing, software | |
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | |
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | +/// See the License for the specific language governing permissions and | |
14 | +/// limitations under the License. | |
15 | +/// | |
16 | + | |
17 | +import { | |
18 | + Component, | |
19 | + ComponentFactoryResolver, | |
20 | + EventEmitter, | |
21 | + Input, | |
22 | + OnDestroy, | |
23 | + OnInit, | |
24 | + Output, | |
25 | + ViewChild | |
26 | +} from '@angular/core'; | |
27 | +import { PageComponent } from '@shared/components/page.component'; | |
28 | +import { Store } from '@ngrx/store'; | |
29 | +import { AppState } from '@core/core.state'; | |
30 | +import { EntityTableConfig } from '@shared/components/entity/entities-table-config.models'; | |
31 | +import { BaseData, HasId } from '@shared/models/base-data'; | |
32 | +import { | |
33 | + EntityType, | |
34 | + EntityTypeResource, | |
35 | + EntityTypeTranslation | |
36 | +} from '@shared/models/entity-type.models'; | |
37 | +import { NgForm } from '@angular/forms'; | |
38 | +import { EntityComponent } from '@shared/components/entity/entity.component'; | |
39 | +import { TbAnchorComponent } from '@shared/components/tb-anchor.component'; | |
40 | +import { EntityAction } from '@shared/components/entity/entity-component.models'; | |
41 | +import { Subscription } from 'rxjs'; | |
42 | +// import { AuditLogMode } from '@shared/models/audit-log.models'; | |
43 | + | |
44 | +@Component({ | |
45 | + selector: 'tb-entity-details-panel', | |
46 | + templateUrl: './entity-details-panel.component.html', | |
47 | + styleUrls: ['./entity-details-panel.component.scss'] | |
48 | +}) | |
49 | +export class EntityDetailsPanelComponent extends PageComponent implements OnInit, OnDestroy { | |
50 | + | |
51 | + @Input() entitiesTableConfig: EntityTableConfig<BaseData<HasId>>; | |
52 | + | |
53 | + @Output() | |
54 | + closeEntityDetails = new EventEmitter<void>(); | |
55 | + | |
56 | + @Output() | |
57 | + entityUpdated = new EventEmitter<BaseData<HasId>>(); | |
58 | + | |
59 | + @Output() | |
60 | + entityAction = new EventEmitter<EntityAction<BaseData<HasId>>>(); | |
61 | + | |
62 | + entityComponent: EntityComponent<BaseData<HasId>>; | |
63 | + detailsForm: NgForm; | |
64 | + | |
65 | + isEditValue = false; | |
66 | + selectedTab = 0; | |
67 | + | |
68 | + entityTypes = EntityType; | |
69 | + | |
70 | + @ViewChild('entityDetailsForm', {static: true}) entityDetailsFormAnchor: TbAnchorComponent; | |
71 | + | |
72 | + translations: EntityTypeTranslation; | |
73 | + resources: EntityTypeResource; | |
74 | + entity: BaseData<HasId>; | |
75 | + | |
76 | + private currentEntityId: HasId; | |
77 | + private entityActionSubscription: Subscription; | |
78 | + | |
79 | + constructor(protected store: Store<AppState>, | |
80 | + private componentFactoryResolver: ComponentFactoryResolver) { | |
81 | + super(store); | |
82 | + } | |
83 | + | |
84 | + @Input() | |
85 | + set entityId(entityId: HasId) { | |
86 | + if (entityId && entityId !== this.currentEntityId) { | |
87 | + this.currentEntityId = entityId; | |
88 | + this.reload(); | |
89 | + } | |
90 | + } | |
91 | + | |
92 | + set isEdit(val: boolean) { | |
93 | + this.isEditValue = val; | |
94 | + this.entityComponent.isEdit = val; | |
95 | + } | |
96 | + | |
97 | + get isEdit() { | |
98 | + return this.isEditValue; | |
99 | + } | |
100 | + | |
101 | + ngOnInit(): void { | |
102 | + this.translations = this.entitiesTableConfig.entityTranslations; | |
103 | + this.resources = this.entitiesTableConfig.entityResources; | |
104 | + this.buildEntityComponent(); | |
105 | + } | |
106 | + | |
107 | + ngOnDestroy(): void { | |
108 | + super.ngOnDestroy(); | |
109 | + if (this.entityActionSubscription) { | |
110 | + this.entityActionSubscription.unsubscribe(); | |
111 | + } | |
112 | + } | |
113 | + | |
114 | + buildEntityComponent() { | |
115 | + const componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.entitiesTableConfig.entityComponent); | |
116 | + const viewContainerRef = this.entityDetailsFormAnchor.viewContainerRef; | |
117 | + viewContainerRef.clear(); | |
118 | + const componentRef = viewContainerRef.createComponent(componentFactory); | |
119 | + this.entityComponent = componentRef.instance; | |
120 | + this.entityComponent.isEdit = this.isEdit; | |
121 | + this.entityComponent.entitiesTableConfig = this.entitiesTableConfig; | |
122 | + this.detailsForm = this.entityComponent.entityNgForm; | |
123 | + this.entityActionSubscription = this.entityComponent.entityAction.subscribe((action) => { | |
124 | + this.entityAction.emit(action); | |
125 | + }); | |
126 | + } | |
127 | + | |
128 | + reload(): void { | |
129 | + this.isEdit = false; | |
130 | + this.entitiesTableConfig.loadEntity(this.currentEntityId).subscribe( | |
131 | + (entity) => { | |
132 | + this.entity = entity; | |
133 | + this.entityComponent.entity = entity; | |
134 | + } | |
135 | + ); | |
136 | + } | |
137 | + | |
138 | + onCloseEntityDetails() { | |
139 | + this.closeEntityDetails.emit(); | |
140 | + } | |
141 | + | |
142 | + onToggleEditMode(isEdit: boolean) { | |
143 | + this.isEdit = isEdit; | |
144 | + if (!this.isEdit) { | |
145 | + this.entityComponent.entity = this.entity; | |
146 | + } else { | |
147 | + this.selectedTab = 0; | |
148 | + } | |
149 | + } | |
150 | + | |
151 | + saveEntity() { | |
152 | + if (this.detailsForm.valid) { | |
153 | + const editingEntity = {...this.entity, ...this.entityComponent.entityFormValue()}; | |
154 | + this.entitiesTableConfig.saveEntity(editingEntity).subscribe( | |
155 | + (entity) => { | |
156 | + this.entity = entity; | |
157 | + this.entityComponent.entity = entity; | |
158 | + this.isEdit = false; | |
159 | + this.entityUpdated.emit(this.entity); | |
160 | + } | |
161 | + ); | |
162 | + } | |
163 | + } | |
164 | + | |
165 | +} | ... | ... |
1 | +/// | |
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | |
3 | +/// | |
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | +/// you may not use this file except in compliance with the License. | |
6 | +/// You may obtain a copy of the License at | |
7 | +/// | |
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | |
9 | +/// | |
10 | +/// Unless required by applicable law or agreed to in writing, software | |
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | |
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | +/// See the License for the specific language governing permissions and | |
14 | +/// limitations under the License. | |
15 | +/// | |
16 | + | |
17 | +import { BaseData, HasId } from '@shared/models/base-data'; | |
18 | +import { PageComponent } from '@shared/components/page.component'; | |
19 | +import { Input, OnInit } from '@angular/core'; | |
20 | +import { Store } from '@ngrx/store'; | |
21 | +import { AppState } from '@core/core.state'; | |
22 | +import { EntityTableConfig } from '@shared/components/entity/entities-table-config.models'; | |
23 | + | |
24 | +export abstract class EntityTableHeaderComponent<T extends BaseData<HasId>> extends PageComponent implements OnInit { | |
25 | + | |
26 | + @Input() | |
27 | + entitiesTableConfig: EntityTableConfig<T>; | |
28 | + | |
29 | + protected constructor(protected store: Store<AppState>) { | |
30 | + super(store); | |
31 | + } | |
32 | + | |
33 | + ngOnInit() { | |
34 | + } | |
35 | + | |
36 | +} | ... | ... |
1 | +/// | |
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | |
3 | +/// | |
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | +/// you may not use this file except in compliance with the License. | |
6 | +/// You may obtain a copy of the License at | |
7 | +/// | |
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | |
9 | +/// | |
10 | +/// Unless required by applicable law or agreed to in writing, software | |
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | |
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | +/// See the License for the specific language governing permissions and | |
14 | +/// limitations under the License. | |
15 | +/// | |
16 | + | |
17 | +import { BaseData, HasId } from '@shared/models/base-data'; | |
18 | +import { FormGroup, NgForm } from '@angular/forms'; | |
19 | +import { PageComponent } from '@shared/components/page.component'; | |
20 | +import { EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'; | |
21 | +import { Store } from '@ngrx/store'; | |
22 | +import { AppState } from '@core/core.state'; | |
23 | +import { EntityAction } from '@shared/components/entity/entity-component.models'; | |
24 | +import { EntityTableConfig } from '@shared/components/entity/entities-table-config.models'; | |
25 | + | |
26 | +export abstract class EntityComponent<T extends BaseData<HasId>> extends PageComponent implements OnInit { | |
27 | + | |
28 | + entityValue: T; | |
29 | + entityForm: FormGroup; | |
30 | + | |
31 | + @ViewChild('entityNgForm', {static: true}) entityNgForm: NgForm; | |
32 | + | |
33 | + isEditValue: boolean; | |
34 | + | |
35 | + @Input() | |
36 | + set isEdit(isEdit: boolean) { | |
37 | + this.isEditValue = isEdit; | |
38 | + this.updateFormState(); | |
39 | + } | |
40 | + | |
41 | + get isEdit() { | |
42 | + return this.isEditValue; | |
43 | + } | |
44 | + | |
45 | + get isAdd(): boolean { | |
46 | + return this.entityValue && !this.entityValue.id; | |
47 | + } | |
48 | + | |
49 | + @Input() | |
50 | + set entity(entity: T) { | |
51 | + this.entityValue = entity; | |
52 | + if (this.entityForm) { | |
53 | + this.entityForm.reset(); | |
54 | + this.updateForm(entity); | |
55 | + } | |
56 | + } | |
57 | + | |
58 | + get entity(): T { | |
59 | + return this.entityValue; | |
60 | + } | |
61 | + | |
62 | + @Input() | |
63 | + entitiesTableConfig: EntityTableConfig<T>; | |
64 | + | |
65 | + @Output() | |
66 | + entityAction = new EventEmitter<EntityAction<T>>(); | |
67 | + | |
68 | + protected constructor(protected store: Store<AppState>) { | |
69 | + super(store); | |
70 | + } | |
71 | + | |
72 | + ngOnInit() { | |
73 | + this.entityForm = this.buildForm(this.entityValue); | |
74 | + } | |
75 | + | |
76 | + onEntityAction($event: Event, action: string) { | |
77 | + const entityAction = {event: $event, action, entity: this.entity} as EntityAction<T>; | |
78 | + let handled = false; | |
79 | + if (this.entitiesTableConfig) { | |
80 | + handled = this.entitiesTableConfig.onEntityAction(entityAction); | |
81 | + } | |
82 | + if (!handled) { | |
83 | + this.entityAction.emit(entityAction); | |
84 | + } | |
85 | + } | |
86 | + | |
87 | + updateFormState() { | |
88 | + if (this.entityForm) { | |
89 | + if (this.isEditValue) { | |
90 | + this.entityForm.enable({emitEvent: false}); | |
91 | + } else { | |
92 | + this.entityForm.disable({emitEvent: false}); | |
93 | + } | |
94 | + } | |
95 | + } | |
96 | + | |
97 | + entityFormValue() { | |
98 | + const formValue = this.entityForm ? {...this.entityForm.value} : {}; | |
99 | + return this.prepareFormValue(formValue); | |
100 | + } | |
101 | + | |
102 | + prepareFormValue(formValue: any): any { | |
103 | + return formValue; | |
104 | + } | |
105 | + | |
106 | + abstract buildForm(entity: T): FormGroup; | |
107 | + | |
108 | + abstract updateForm(entity: T); | |
109 | + | |
110 | +} | ... | ... |
1 | +<!-- | |
2 | + | |
3 | + Copyright © 2016-2019 The Thingsboard Authors | |
4 | + | |
5 | + Licensed under the Apache License, Version 2.0 (the "License"); | |
6 | + you may not use this file except in compliance with the License. | |
7 | + You may obtain a copy of the License at | |
8 | + | |
9 | + http://www.apache.org/licenses/LICENSE-2.0 | |
10 | + | |
11 | + Unless required by applicable law or agreed to in writing, software | |
12 | + distributed under the License is distributed on an "AS IS" BASIS, | |
13 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
14 | + See the License for the specific language governing permissions and | |
15 | + limitations under the License. | |
16 | + | |
17 | +--> | |
18 | +<section fxLayout="column" fxLayoutAlign="start start"> | |
19 | + <section fxLayout="row" fxLayoutAlign="start start" fxLayoutGap="16px"> | |
20 | + <mat-form-field> | |
21 | + <mat-placeholder translate>datetime.date-from</mat-placeholder> | |
22 | + <mat-datetimepicker-toggle [for]="startDatePicker" matPrefix></mat-datetimepicker-toggle> | |
23 | + <mat-datetimepicker #startDatePicker type="date" openOnFocus="true"></mat-datetimepicker> | |
24 | + <input matInput [(ngModel)]="startDate" [matDatetimepicker]="startDatePicker" (ngModelChange)="onStartDateChange()"> | |
25 | + </mat-form-field> | |
26 | + <mat-form-field> | |
27 | + <mat-placeholder translate>datetime.time-from</mat-placeholder> | |
28 | + <mat-datetimepicker-toggle [for]="startTimePicker" matPrefix></mat-datetimepicker-toggle> | |
29 | + <mat-datetimepicker #startTimePicker type="time" openOnFocus="true"></mat-datetimepicker> | |
30 | + <input matInput [(ngModel)]="startDate" [matDatetimepicker]="startTimePicker" (ngModelChange)="onStartDateChange()"> | |
31 | + </mat-form-field> | |
32 | + </section> | |
33 | + <section fxLayout="row" fxLayoutAlign="start start" fxLayoutGap="16px"> | |
34 | + <mat-form-field> | |
35 | + <mat-placeholder translate>datetime.date-to</mat-placeholder> | |
36 | + <mat-datetimepicker-toggle [for]="endDatePicker" matPrefix></mat-datetimepicker-toggle> | |
37 | + <mat-datetimepicker #endDatePicker type="date" openOnFocus="true"></mat-datetimepicker> | |
38 | + <input matInput [(ngModel)]="endDate" [matDatetimepicker]="endDatePicker" (ngModelChange)="onEndDateChange()"> | |
39 | + </mat-form-field> | |
40 | + <mat-form-field> | |
41 | + <mat-placeholder translate>datetime.time-to</mat-placeholder> | |
42 | + <mat-datetimepicker-toggle [for]="endTimePicker" matPrefix></mat-datetimepicker-toggle> | |
43 | + <mat-datetimepicker #endTimePicker type="time" openOnFocus="true"></mat-datetimepicker> | |
44 | + <input matInput [(ngModel)]="endDate" [matDatetimepicker]="endTimePicker" (ngModelChange)="onEndDateChange()"> | |
45 | + </mat-form-field> | |
46 | + </section> | |
47 | +</section> | ... | ... |
1 | +/** | |
2 | + * Copyright © 2016-2019 The Thingsboard Authors | |
3 | + * | |
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + * you may not use this file except in compliance with the License. | |
6 | + * You may obtain a copy of the License at | |
7 | + * | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + * | |
10 | + * Unless required by applicable law or agreed to in writing, software | |
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + * See the License for the specific language governing permissions and | |
14 | + * limitations under the License. | |
15 | + */ | |
16 | +:host ::ng-deep { | |
17 | + .mat-form-field-wrapper { | |
18 | + padding-bottom: 8px; | |
19 | + } | |
20 | + .mat-form-field-underline { | |
21 | + bottom: 8px; | |
22 | + } | |
23 | + .mat-form-field-infix { | |
24 | + width: 150px; | |
25 | + } | |
26 | +} | ... | ... |
1 | +/// | |
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | |
3 | +/// | |
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | +/// you may not use this file except in compliance with the License. | |
6 | +/// You may obtain a copy of the License at | |
7 | +/// | |
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | |
9 | +/// | |
10 | +/// Unless required by applicable law or agreed to in writing, software | |
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | |
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | +/// See the License for the specific language governing permissions and | |
14 | +/// limitations under the License. | |
15 | +/// | |
16 | + | |
17 | +import { Component, forwardRef, Input, OnInit } from '@angular/core'; | |
18 | +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; | |
19 | +import { FixedWindow } from '@shared/models/time/time.models'; | |
20 | + | |
21 | +@Component({ | |
22 | + selector: 'tb-datetime-period', | |
23 | + templateUrl: './datetime-period.component.html', | |
24 | + styleUrls: ['./datetime-period.component.scss'], | |
25 | + providers: [ | |
26 | + { | |
27 | + provide: NG_VALUE_ACCESSOR, | |
28 | + useExisting: forwardRef(() => DatetimePeriodComponent), | |
29 | + multi: true | |
30 | + } | |
31 | + ] | |
32 | +}) | |
33 | +export class DatetimePeriodComponent implements OnInit, ControlValueAccessor { | |
34 | + | |
35 | + @Input() disabled: boolean; | |
36 | + | |
37 | + modelValue: FixedWindow; | |
38 | + | |
39 | + startDate: Date; | |
40 | + endDate: Date; | |
41 | + | |
42 | + endTime: any; | |
43 | + | |
44 | + maxStartDate: Date; | |
45 | + minEndDate: Date; | |
46 | + maxEndDate: Date; | |
47 | + | |
48 | + changePending = false; | |
49 | + | |
50 | + private propagateChange = null; | |
51 | + | |
52 | + constructor() { | |
53 | + } | |
54 | + | |
55 | + ngOnInit(): void { | |
56 | + } | |
57 | + | |
58 | + registerOnChange(fn: any): void { | |
59 | + this.propagateChange = fn; | |
60 | + if (this.changePending && this.propagateChange) { | |
61 | + this.changePending = false; | |
62 | + this.propagateChange(this.modelValue); | |
63 | + } | |
64 | + } | |
65 | + | |
66 | + registerOnTouched(fn: any): void { | |
67 | + } | |
68 | + | |
69 | + setDisabledState(isDisabled: boolean): void { | |
70 | + this.disabled = isDisabled; | |
71 | + } | |
72 | + | |
73 | + writeValue(datePeriod: FixedWindow): void { | |
74 | + this.modelValue = datePeriod; | |
75 | + if (this.modelValue) { | |
76 | + this.startDate = new Date(this.modelValue.startTimeMs); | |
77 | + this.endDate = new Date(this.modelValue.endTimeMs); | |
78 | + } else { | |
79 | + const date = new Date(); | |
80 | + this.startDate = new Date( | |
81 | + date.getFullYear(), | |
82 | + date.getMonth(), | |
83 | + date.getDate() - 1, | |
84 | + date.getHours(), | |
85 | + date.getMinutes(), | |
86 | + date.getSeconds(), | |
87 | + date.getMilliseconds()); | |
88 | + this.endDate = date; | |
89 | + this.updateView(); | |
90 | + } | |
91 | + this.updateMinMaxDates(); | |
92 | + } | |
93 | + | |
94 | + updateView() { | |
95 | + let value: FixedWindow = null; | |
96 | + if (this.startDate && this.endDate) { | |
97 | + value = new FixedWindow(); | |
98 | + value.startTimeMs = this.startDate.getTime(); | |
99 | + value.endTimeMs = this.endDate.getTime(); | |
100 | + } | |
101 | + this.modelValue = value; | |
102 | + if (!this.propagateChange) { | |
103 | + this.changePending = true; | |
104 | + } else { | |
105 | + this.propagateChange(this.modelValue); | |
106 | + } | |
107 | + } | |
108 | + | |
109 | + updateMinMaxDates() { | |
110 | + this.maxStartDate = new Date(this.endDate.getTime() - 1000); | |
111 | + this.minEndDate = new Date(this.startDate.getTime() + 1000); | |
112 | + this.maxEndDate = new Date(); | |
113 | + } | |
114 | + | |
115 | + onStartDateChange() { | |
116 | + if (this.startDate) { | |
117 | + if (this.startDate.getTime() > this.maxStartDate.getTime()) { | |
118 | + this.startDate = new Date(this.maxStartDate.getTime()); | |
119 | + } | |
120 | + this.updateMinMaxDates(); | |
121 | + } | |
122 | + this.updateView(); | |
123 | + } | |
124 | + | |
125 | + onEndDateChange() { | |
126 | + if (this.endDate) { | |
127 | + if (this.endDate.getTime() < this.minEndDate.getTime()) { | |
128 | + this.endDate = new Date(this.minEndDate.getTime()); | |
129 | + } else if (this.endDate.getTime() > this.maxEndDate.getTime()) { | |
130 | + this.endDate = new Date(this.maxEndDate.getTime()); | |
131 | + } | |
132 | + this.updateMinMaxDates(); | |
133 | + } | |
134 | + this.updateView(); | |
135 | + } | |
136 | + | |
137 | +} | ... | ... |
1 | +<!-- | |
2 | + | |
3 | + Copyright © 2016-2019 The Thingsboard Authors | |
4 | + | |
5 | + Licensed under the Apache License, Version 2.0 (the "License"); | |
6 | + you may not use this file except in compliance with the License. | |
7 | + You may obtain a copy of the License at | |
8 | + | |
9 | + http://www.apache.org/licenses/LICENSE-2.0 | |
10 | + | |
11 | + Unless required by applicable law or agreed to in writing, software | |
12 | + distributed under the License is distributed on an "AS IS" BASIS, | |
13 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
14 | + See the License for the specific language governing permissions and | |
15 | + limitations under the License. | |
16 | + | |
17 | +--> | |
18 | +<section fxLayout="row"> | |
19 | + <section class="interval-section" fxLayout="column" fxFlex [fxShow]="advanced"> | |
20 | + <label class="tb-small interval-label" translate>{{ predefinedName }}</label> | |
21 | + <section fxLayout="row" fxLayoutAlign="start start" fxFlex fxLayoutGap="6px"> | |
22 | + <mat-form-field class="number-input"> | |
23 | + <mat-label translate>timeinterval.days</mat-label> | |
24 | + <input matInput type="number" step="1" min="0" [(ngModel)]="days" (ngModelChange)="onTimeInputChange('days')"/> | |
25 | + </mat-form-field> | |
26 | + <mat-form-field class="number-input"> | |
27 | + <mat-label translate>timeinterval.hours</mat-label> | |
28 | + <input matInput type="number" step="1" [(ngModel)]="hours" (ngModelChange)="onTimeInputChange('hours')"/> | |
29 | + </mat-form-field> | |
30 | + <mat-form-field class="number-input"> | |
31 | + <mat-label translate>timeinterval.minutes</mat-label> | |
32 | + <input matInput type="number" step="1" [(ngModel)]="mins" (ngModelChange)="onTimeInputChange('mins')"/> | |
33 | + </mat-form-field> | |
34 | + <mat-form-field class="number-input"> | |
35 | + <mat-label translate>timeinterval.seconds</mat-label> | |
36 | + <input matInput type="number" step="1" [(ngModel)]="secs" (ngModelChange)="onTimeInputChange('secs')"/> | |
37 | + </mat-form-field> | |
38 | + </section> | |
39 | + </section> | |
40 | + <section class="interval-section" fxLayout="row" fxFlex [fxShow]="!advanced"> | |
41 | + <mat-form-field fxFlex> | |
42 | + <mat-label translate>{{ predefinedName }}</mat-label> | |
43 | + <mat-select matInput [(ngModel)]="intervalMs" (ngModelChange)="onIntervalMsChange()" style="min-width: 150px;"> | |
44 | + <mat-option *ngFor="let interval of intervals" [value]="interval.value"> | |
45 | + {{ interval.name | translate:interval.translateParams }} | |
46 | + </mat-option> | |
47 | + </mat-select> | |
48 | + </mat-form-field> | |
49 | + </section> | |
50 | + <section fxLayout="column" fxLayoutAlign="center center"> | |
51 | + <label class="tb-small advanced-label" translate>timeinterval.advanced</label> | |
52 | + <mat-slide-toggle class="advanced-switch" [(ngModel)]="advanced" (ngModelChange)="onAdvancedChange()"></mat-slide-toggle> | |
53 | + </section> | |
54 | +</section> | ... | ... |
1 | +/** | |
2 | + * Copyright © 2016-2019 The Thingsboard Authors | |
3 | + * | |
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + * you may not use this file except in compliance with the License. | |
6 | + * You may obtain a copy of the License at | |
7 | + * | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + * | |
10 | + * Unless required by applicable law or agreed to in writing, software | |
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + * See the License for the specific language governing permissions and | |
14 | + * limitations under the License. | |
15 | + */ | |
16 | +:host { | |
17 | + min-width: 355px; | |
18 | + | |
19 | + .advanced-switch { | |
20 | + margin-bottom: 16px; | |
21 | + } | |
22 | + | |
23 | + .advanced-label { | |
24 | + margin: 5px 0; | |
25 | + } | |
26 | + | |
27 | + .interval-section { | |
28 | + min-height: 66px; | |
29 | + .interval-label { | |
30 | + margin-bottom: 7px; | |
31 | + margin-top: -1px; | |
32 | + } | |
33 | + } | |
34 | + | |
35 | +} | |
36 | + | |
37 | +:host ::ng-deep { | |
38 | + .number-input { | |
39 | + .mat-form-field-infix { | |
40 | + width: 70px; | |
41 | + } | |
42 | + } | |
43 | +} | ... | ... |
1 | +/// | |
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | |
3 | +/// | |
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | +/// you may not use this file except in compliance with the License. | |
6 | +/// You may obtain a copy of the License at | |
7 | +/// | |
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | |
9 | +/// | |
10 | +/// Unless required by applicable law or agreed to in writing, software | |
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | |
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | +/// See the License for the specific language governing permissions and | |
14 | +/// limitations under the License. | |
15 | +/// | |
16 | + | |
17 | +import { ChangeDetectorRef, Component, forwardRef, Input, OnInit } from '@angular/core'; | |
18 | +import { | |
19 | + ControlValueAccessor, | |
20 | + FormControl, | |
21 | + NG_VALIDATORS, | |
22 | + NG_VALUE_ACCESSOR, | |
23 | + Validator | |
24 | +} from '@angular/forms'; | |
25 | +import { Timewindow } from '@shared/models/time/time.models'; | |
26 | +import { TimeInterval, TimeService } from '@core/services/time.service'; | |
27 | + | |
28 | +@Component({ | |
29 | + selector: 'tb-timeinterval', | |
30 | + templateUrl: './timeinterval.component.html', | |
31 | + styleUrls: ['./timeinterval.component.scss'], | |
32 | + providers: [ | |
33 | + { | |
34 | + provide: NG_VALUE_ACCESSOR, | |
35 | + useExisting: forwardRef(() => TimeintervalComponent), | |
36 | + multi: true | |
37 | + } | |
38 | + ] | |
39 | +}) | |
40 | +export class TimeintervalComponent implements OnInit, ControlValueAccessor { | |
41 | + | |
42 | + minValue: number; | |
43 | + maxValue: number; | |
44 | + | |
45 | + @Input() | |
46 | + set min(min: number) { | |
47 | + if (typeof min !== 'undefined' && min !== this.minValue) { | |
48 | + this.minValue = min; | |
49 | + this.updateView(); | |
50 | + } | |
51 | + } | |
52 | + | |
53 | + @Input() | |
54 | + set max(max: number) { | |
55 | + if (typeof max !== 'undefined' && max !== this.maxValue) { | |
56 | + this.maxValue = max; | |
57 | + this.updateView(); | |
58 | + } | |
59 | + } | |
60 | + | |
61 | + @Input() predefinedName: string; | |
62 | + @Input() disabled: boolean; | |
63 | + | |
64 | + days = 0; | |
65 | + hours = 0; | |
66 | + mins = 1; | |
67 | + secs = 0; | |
68 | + | |
69 | + intervalMs = 0; | |
70 | + modelValue: number; | |
71 | + | |
72 | + advanced = false; | |
73 | + rendered = false; | |
74 | + | |
75 | + intervals: Array<TimeInterval>; | |
76 | + | |
77 | + private propagateChange = (_: any) => {}; | |
78 | + | |
79 | + constructor(private timeService: TimeService) { | |
80 | + } | |
81 | + | |
82 | + ngOnInit(): void { | |
83 | + this.boundInterval(); | |
84 | + } | |
85 | + | |
86 | + registerOnChange(fn: any): void { | |
87 | + this.propagateChange = fn; | |
88 | + } | |
89 | + | |
90 | + registerOnTouched(fn: any): void { | |
91 | + } | |
92 | + | |
93 | + setDisabledState(isDisabled: boolean): void { | |
94 | + this.disabled = isDisabled; | |
95 | + } | |
96 | + | |
97 | + writeValue(intervalMs: number): void { | |
98 | + this.modelValue = intervalMs; | |
99 | + this.rendered = true; | |
100 | + if (typeof this.modelValue !== 'undefined') { | |
101 | + const min = this.timeService.boundMinInterval(this.minValue); | |
102 | + const max = this.timeService.boundMaxInterval(this.maxValue); | |
103 | + if (this.modelValue >= min && this.modelValue <= max) { | |
104 | + this.advanced = !this.timeService.matchesExistingInterval(this.minValue, this.maxValue, this.modelValue); | |
105 | + this.setIntervalMs(this.modelValue); | |
106 | + } else { | |
107 | + this.boundInterval(); | |
108 | + } | |
109 | + } | |
110 | + } | |
111 | + | |
112 | + setIntervalMs(intervalMs: number) { | |
113 | + if (!this.advanced) { | |
114 | + this.intervalMs = intervalMs; | |
115 | + } | |
116 | + const intervalSeconds = Math.floor(intervalMs / 1000); | |
117 | + this.days = Math.floor(intervalSeconds / 86400); | |
118 | + this.hours = Math.floor((intervalSeconds % 86400) / 3600); | |
119 | + this.mins = Math.floor(((intervalSeconds % 86400) % 3600) / 60); | |
120 | + this.secs = intervalSeconds % 60; | |
121 | + } | |
122 | + | |
123 | + boundInterval() { | |
124 | + const min = this.timeService.boundMinInterval(this.minValue); | |
125 | + const max = this.timeService.boundMaxInterval(this.maxValue); | |
126 | + this.intervals = this.timeService.getIntervals(this.minValue, this.maxValue); | |
127 | + if (this.rendered) { | |
128 | + let newIntervalMs = this.modelValue; | |
129 | + if (newIntervalMs < min) { | |
130 | + newIntervalMs = min; | |
131 | + } else if (newIntervalMs > max) { | |
132 | + newIntervalMs = max; | |
133 | + } | |
134 | + if (!this.advanced) { | |
135 | + newIntervalMs = this.timeService.boundToPredefinedInterval(min, max, newIntervalMs); | |
136 | + } | |
137 | + if (newIntervalMs !== this.modelValue) { | |
138 | + this.setIntervalMs(newIntervalMs); | |
139 | + this.updateView(); | |
140 | + } | |
141 | + } | |
142 | + } | |
143 | + | |
144 | + updateView() { | |
145 | + if (!this.rendered) { | |
146 | + return; | |
147 | + } | |
148 | + let value = null; | |
149 | + let intervalMs; | |
150 | + if (!this.advanced) { | |
151 | + intervalMs = this.intervalMs; | |
152 | + if (!intervalMs || isNaN(intervalMs)) { | |
153 | + intervalMs = this.calculateIntervalMs(); | |
154 | + } | |
155 | + } else { | |
156 | + intervalMs = this.calculateIntervalMs(); | |
157 | + } | |
158 | + if (!isNaN(intervalMs) && intervalMs > 0) { | |
159 | + value = intervalMs; | |
160 | + } | |
161 | + this.modelValue = value; | |
162 | + this.propagateChange(this.modelValue); | |
163 | + this.boundInterval(); | |
164 | + } | |
165 | + | |
166 | + calculateIntervalMs(): number { | |
167 | + return (this.days * 86400 + | |
168 | + this.hours * 3600 + | |
169 | + this.mins * 60 + | |
170 | + this.secs) * 1000; | |
171 | + } | |
172 | + | |
173 | + onIntervalMsChange() { | |
174 | + this.updateView(); | |
175 | + } | |
176 | + | |
177 | + onAdvancedChange() { | |
178 | + if (!this.advanced) { | |
179 | + this.intervalMs = this.calculateIntervalMs(); | |
180 | + } else { | |
181 | + let intervalMs = this.intervalMs; | |
182 | + if (!intervalMs || isNaN(intervalMs)) { | |
183 | + intervalMs = this.calculateIntervalMs(); | |
184 | + } | |
185 | + this.setIntervalMs(intervalMs); | |
186 | + } | |
187 | + this.updateView(); | |
188 | + } | |
189 | + | |
190 | + onTimeInputChange(type: string) { | |
191 | + switch (type) { | |
192 | + case 'secs': | |
193 | + setTimeout(() => this.onSecsChange(), 0); | |
194 | + break; | |
195 | + case 'mins': | |
196 | + setTimeout(() => this.onMinsChange(), 0); | |
197 | + break; | |
198 | + case 'hours': | |
199 | + setTimeout(() => this.onHoursChange(), 0); | |
200 | + break; | |
201 | + case 'days': | |
202 | + setTimeout(() => this.onDaysChange(), 0); | |
203 | + break; | |
204 | + } | |
205 | + } | |
206 | + | |
207 | + onSecsChange() { | |
208 | + if (typeof this.secs === 'undefined') { | |
209 | + return; | |
210 | + } | |
211 | + if (this.secs < 0) { | |
212 | + if ((this.days + this.hours + this.mins) > 0) { | |
213 | + this.secs = this.secs + 60; | |
214 | + this.mins--; | |
215 | + this.onMinsChange(); | |
216 | + } else { | |
217 | + this.secs = 0; | |
218 | + } | |
219 | + } else if (this.secs >= 60) { | |
220 | + this.secs = this.secs - 60; | |
221 | + this.mins++; | |
222 | + this.onMinsChange(); | |
223 | + } | |
224 | + this.updateView(); | |
225 | + } | |
226 | + | |
227 | + onMinsChange() { | |
228 | + if (typeof this.mins === 'undefined') { | |
229 | + return; | |
230 | + } | |
231 | + if (this.mins < 0) { | |
232 | + if ((this.days + this.hours) > 0) { | |
233 | + this.mins = this.mins + 60; | |
234 | + this.hours--; | |
235 | + this.onHoursChange(); | |
236 | + } else { | |
237 | + this.mins = 0; | |
238 | + } | |
239 | + } else if (this.mins >= 60) { | |
240 | + this.mins = this.mins - 60; | |
241 | + this.hours++; | |
242 | + this.onHoursChange(); | |
243 | + } | |
244 | + this.updateView(); | |
245 | + } | |
246 | + | |
247 | + onHoursChange() { | |
248 | + if (typeof this.hours === 'undefined') { | |
249 | + return; | |
250 | + } | |
251 | + if (this.hours < 0) { | |
252 | + if (this.days > 0) { | |
253 | + this.hours = this.hours + 24; | |
254 | + this.days--; | |
255 | + this.onDaysChange(); | |
256 | + } else { | |
257 | + this.hours = 0; | |
258 | + } | |
259 | + } else if (this.hours >= 24) { | |
260 | + this.hours = this.hours - 24; | |
261 | + this.days++; | |
262 | + this.onDaysChange(); | |
263 | + } | |
264 | + this.updateView(); | |
265 | + } | |
266 | + | |
267 | + onDaysChange() { | |
268 | + if (typeof this.days === 'undefined') { | |
269 | + return; | |
270 | + } | |
271 | + if (this.days < 0) { | |
272 | + this.days = 0; | |
273 | + } | |
274 | + this.updateView(); | |
275 | + } | |
276 | + | |
277 | +} | ... | ... |
1 | +<!-- | |
2 | + | |
3 | + Copyright © 2016-2019 The Thingsboard Authors | |
4 | + | |
5 | + Licensed under the Apache License, Version 2.0 (the "License"); | |
6 | + you may not use this file except in compliance with the License. | |
7 | + You may obtain a copy of the License at | |
8 | + | |
9 | + http://www.apache.org/licenses/LICENSE-2.0 | |
10 | + | |
11 | + Unless required by applicable law or agreed to in writing, software | |
12 | + distributed under the License is distributed on an "AS IS" BASIS, | |
13 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
14 | + See the License for the specific language governing permissions and | |
15 | + limitations under the License. | |
16 | + | |
17 | +--> | |
18 | +<form [formGroup]="timewindowForm" (ngSubmit)="update()"> | |
19 | + <fieldset [disabled]="(isLoading$ | async)"> | |
20 | + <div class="mat-content" style="height: 100%;" fxFlex fxLayout="column"> | |
21 | + <section fxLayout="column"> | |
22 | + <mat-tab-group dynamicHeight [ngClass]="{'tb-headless': historyOnly}" | |
23 | + (selectedIndexChange)="timewindowForm.markAsDirty()" [(selectedIndex)]="timewindow.selectedTab"> | |
24 | + <mat-tab label="{{ 'timewindow.realtime' | translate }}"> | |
25 | + <div formGroupName="realtime" class="mat-content mat-padding" fxLayout="column"> | |
26 | + <tb-timeinterval | |
27 | + formControlName="timewindowMs" | |
28 | + predefinedName="timewindow.last" | |
29 | + [required]="timewindow.selectedTab === timewindowTypes.REALTIME" | |
30 | + style="padding-top: 8px;"></tb-timeinterval> | |
31 | + </div> | |
32 | + </mat-tab> | |
33 | + <mat-tab label="{{ 'timewindow.history' | translate }}"> | |
34 | + <div formGroupName="history" class="mat-content mat-padding" style="padding-top: 8px;"> | |
35 | + <mat-radio-group formControlName="historyType"> | |
36 | + <mat-radio-button [value]="historyTypes.LAST_INTERVAL" color="primary"> | |
37 | + <section fxLayout="column"> | |
38 | + <tb-timeinterval | |
39 | + formControlName="timewindowMs" | |
40 | + predefinedName="timewindow.last" | |
41 | + [fxShow]="timewindowForm.get('history').get('historyType').value === historyTypes.LAST_INTERVAL" | |
42 | + [required]="timewindow.selectedTab === timewindowTypes.HISTORY && | |
43 | + timewindowForm.get('history').get('historyType').value === historyTypes.LAST_INTERVAL" | |
44 | + style="padding-top: 8px;"></tb-timeinterval> | |
45 | + </section> | |
46 | + </mat-radio-button> | |
47 | + <mat-radio-button [value]="historyTypes.FIXED" color="primary"> | |
48 | + <section fxLayout="column"> | |
49 | + <span translate>timewindow.time-period</span> | |
50 | + <tb-datetime-period | |
51 | + formControlName="fixedTimewindow" | |
52 | + [fxShow]="timewindowForm.get('history').get('historyType').value === historyTypes.FIXED" | |
53 | + [required]="timewindow.selectedTab === timewindowTypes.HISTORY && | |
54 | + timewindowForm.get('history').get('historyType').value === historyTypes.FIXED" | |
55 | + style="padding-top: 8px;"></tb-datetime-period> | |
56 | + </section> | |
57 | + </mat-radio-button> | |
58 | + </mat-radio-group> | |
59 | + </div> | |
60 | + </mat-tab> | |
61 | + </mat-tab-group> | |
62 | + <div *ngIf="aggregation" formGroupName="aggregation" class="mat-content mat-padding" fxLayout="column"> | |
63 | + <mat-form-field> | |
64 | + <mat-label translate>aggregation.function</mat-label> | |
65 | + <mat-select matInput formControlName="type" style="min-width: 150px;"> | |
66 | + <mat-option *ngFor="let aggregation of aggregations" [value]="aggregation"> | |
67 | + {{ aggregationTypesTranslations.get(aggregation) | translate }} | |
68 | + </mat-option> | |
69 | + </mat-select> | |
70 | + </mat-form-field> | |
71 | + <div *ngIf="timewindowForm.get('aggregation').get('type').value === aggregationTypes.NONE" | |
72 | + class="limit-slider-container" | |
73 | + fxLayout="row" fxLayoutAlign="start center"> | |
74 | + <span translate>aggregation.limit</span> | |
75 | + <mat-slider fxFlex formControlName="limit" | |
76 | + thumbLabel | |
77 | + [value]="timewindowForm.get('aggregation').get('limit').value" | |
78 | + min="{{minDatapointsLimit()}}" | |
79 | + max="{{maxDatapointsLimit()}}"> | |
80 | + </mat-slider> | |
81 | + <mat-form-field style="max-width: 80px;"> | |
82 | + <input matInput formControlName="limit" type="number" step="1" | |
83 | + [value]="timewindowForm.get('aggregation').get('limit').value" | |
84 | + min="{{minDatapointsLimit()}}" | |
85 | + max="{{maxDatapointsLimit()}}"/> | |
86 | + </mat-form-field> | |
87 | + </div> | |
88 | + </div> | |
89 | + <div formGroupName="realtime" | |
90 | + *ngIf="aggregation && timewindowForm.get('aggregation').get('type').value !== aggregationTypes.NONE && | |
91 | + timewindow.selectedTab === timewindowTypes.REALTIME" class="mat-content mat-padding" fxLayout="column"> | |
92 | + <tb-timeinterval | |
93 | + formControlName="interval" | |
94 | + [min]="minRealtimeAggInterval()" [max]="maxRealtimeAggInterval()" | |
95 | + predefinedName="aggregation.group-interval"> | |
96 | + </tb-timeinterval> | |
97 | + </div> | |
98 | + <div formGroupName="history" | |
99 | + *ngIf="aggregation && timewindowForm.get('aggregation').get('type').value !== aggregationTypes.NONE && | |
100 | + timewindow.selectedTab === timewindowTypes.HISTORY" class="mat-content mat-padding" fxLayout="column"> | |
101 | + <tb-timeinterval | |
102 | + formControlName="interval" | |
103 | + [min]="minHistoryAggInterval()" [max]="maxHistoryAggInterval()" | |
104 | + predefinedName="aggregation.group-interval"> | |
105 | + </tb-timeinterval> | |
106 | + </div> | |
107 | + </section> | |
108 | + <span fxFlex></span> | |
109 | + <div fxLayout="row" class="tb-panel-actions"> | |
110 | + <span fxFlex></span> | |
111 | + <button type="submit" | |
112 | + mat-raised-button | |
113 | + color="primary" | |
114 | + [disabled]="(isLoading$ | async) || timewindowForm.invalid || !timewindowForm.dirty"> | |
115 | + {{ 'action.update' | translate }} | |
116 | + </button> | |
117 | + <button type="button" | |
118 | + mat-button | |
119 | + [disabled]="(isLoading$ | async)" | |
120 | + (click)="cancel()" | |
121 | + style="margin-right: 20px;"> | |
122 | + {{ 'action.cancel' | translate }} | |
123 | + </button> | |
124 | + </div> | |
125 | + </div> | |
126 | + </fieldset> | |
127 | +</form> | ... | ... |
1 | +/** | |
2 | + * Copyright © 2016-2019 The Thingsboard Authors | |
3 | + * | |
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + * you may not use this file except in compliance with the License. | |
6 | + * You may obtain a copy of the License at | |
7 | + * | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + * | |
10 | + * Unless required by applicable law or agreed to in writing, software | |
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + * See the License for the specific language governing permissions and | |
14 | + * limitations under the License. | |
15 | + */ | |
16 | +:host { | |
17 | + width: 100%; | |
18 | + height: 100%; | |
19 | + form, | |
20 | + fieldset { | |
21 | + height: 100%; | |
22 | + } | |
23 | + | |
24 | + .mat-content { | |
25 | + overflow: hidden; | |
26 | + background-color: #fff; | |
27 | + } | |
28 | + | |
29 | + .mat-padding { | |
30 | + padding: 0 16px; | |
31 | + } | |
32 | + | |
33 | + .limit-slider-container { | |
34 | + >:first-child { | |
35 | + margin-right: 16px; | |
36 | + } | |
37 | + >:last-child { | |
38 | + margin-left: 16px; | |
39 | + } | |
40 | + >:first-child, >:last-child { | |
41 | + min-width: 25px; | |
42 | + max-width: 42px; | |
43 | + } | |
44 | + mat-form-field input[type=number] { | |
45 | + text-align: center; | |
46 | + } | |
47 | + } | |
48 | + | |
49 | +} | |
50 | + | |
51 | +:host ::ng-deep { | |
52 | + mat-radio-button { | |
53 | + display: block; | |
54 | + margin-bottom: 16px; | |
55 | + .mat-radio-label { | |
56 | + width: 100%; | |
57 | + align-items: start; | |
58 | + .mat-radio-label-content { | |
59 | + width: 100%; | |
60 | + } | |
61 | + } | |
62 | + } | |
63 | +} | ... | ... |
1 | +/// | |
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | |
3 | +/// | |
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | +/// you may not use this file except in compliance with the License. | |
6 | +/// You may obtain a copy of the License at | |
7 | +/// | |
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | |
9 | +/// | |
10 | +/// Unless required by applicable law or agreed to in writing, software | |
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | |
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | +/// See the License for the specific language governing permissions and | |
14 | +/// limitations under the License. | |
15 | +/// | |
16 | + | |
17 | +import { | |
18 | + Component, | |
19 | + Inject, | |
20 | + InjectionToken, | |
21 | + OnInit, | |
22 | + ViewChild, | |
23 | + ViewContainerRef | |
24 | +} from '@angular/core'; | |
25 | +import { TranslateService } from '@ngx-translate/core'; | |
26 | +import { MillisecondsToTimeStringPipe } from '@shared/pipe/milliseconds-to-time-string.pipe'; | |
27 | +import { | |
28 | + Aggregation, | |
29 | + aggregationTranslations, | |
30 | + AggregationType, | |
31 | + HistoryWindow, | |
32 | + HistoryWindowType, | |
33 | + IntervalWindow, | |
34 | + Timewindow, | |
35 | + TimewindowType | |
36 | +} from '@shared/models/time/time.models'; | |
37 | +import { DatePipe } from '@angular/common'; | |
38 | +import { Overlay, OverlayRef } from '@angular/cdk/overlay'; | |
39 | +import { PageComponent } from '@shared/components/page.component'; | |
40 | +import { Store } from '@ngrx/store'; | |
41 | +import { AppState } from '@core/core.state'; | |
42 | +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; | |
43 | +import { TimeService } from '@core/services/time.service'; | |
44 | + | |
45 | +export const TIMEWINDOW_PANEL_DATA = new InjectionToken<any>('TimewindowPanelData'); | |
46 | + | |
47 | +export interface TimewindowPanelData { | |
48 | + historyOnly: boolean; | |
49 | + timewindow: Timewindow; | |
50 | + aggregation: boolean; | |
51 | +} | |
52 | + | |
53 | +@Component({ | |
54 | + selector: 'tb-timewindow-panel', | |
55 | + templateUrl: './timewindow-panel.component.html', | |
56 | + styleUrls: ['./timewindow-panel.component.scss'] | |
57 | +}) | |
58 | +export class TimewindowPanelComponent extends PageComponent implements OnInit { | |
59 | + | |
60 | + historyOnly = false; | |
61 | + | |
62 | + aggregation = false; | |
63 | + | |
64 | + timewindow: Timewindow; | |
65 | + | |
66 | + result: Timewindow; | |
67 | + | |
68 | + timewindowForm: FormGroup; | |
69 | + | |
70 | + historyTypes = HistoryWindowType; | |
71 | + | |
72 | + timewindowTypes = TimewindowType; | |
73 | + | |
74 | + aggregationTypes = AggregationType; | |
75 | + | |
76 | + aggregations = Object.keys(AggregationType); | |
77 | + | |
78 | + aggregationTypesTranslations = aggregationTranslations; | |
79 | + | |
80 | + constructor(@Inject(TIMEWINDOW_PANEL_DATA) public data: TimewindowPanelData, | |
81 | + public overlayRef: OverlayRef, | |
82 | + protected store: Store<AppState>, | |
83 | + public fb: FormBuilder, | |
84 | + private timeService: TimeService, | |
85 | + private translate: TranslateService, | |
86 | + private millisecondsToTimeStringPipe: MillisecondsToTimeStringPipe, | |
87 | + private datePipe: DatePipe, | |
88 | + private overlay: Overlay, | |
89 | + public viewContainerRef: ViewContainerRef) { | |
90 | + super(store); | |
91 | + this.historyOnly = data.historyOnly; | |
92 | + this.timewindow = data.timewindow; | |
93 | + this.aggregation = data.aggregation; | |
94 | + } | |
95 | + | |
96 | + ngOnInit(): void { | |
97 | + this.timewindowForm = this.fb.group({ | |
98 | + realtime: this.fb.group( | |
99 | + { | |
100 | + timewindowMs: [ | |
101 | + this.timewindow.realtime && typeof this.timewindow.realtime.timewindowMs !== 'undefined' | |
102 | + ? this.timewindow.realtime.timewindowMs : null | |
103 | + ], | |
104 | + interval: [ | |
105 | + this.timewindow.realtime && typeof this.timewindow.realtime.interval !== 'undefined' | |
106 | + ? this.timewindow.realtime.interval : null | |
107 | + ] | |
108 | + } | |
109 | + ), | |
110 | + history: this.fb.group( | |
111 | + { | |
112 | + historyType: [ | |
113 | + this.timewindow.history && typeof this.timewindow.history.historyType !== 'undefined' | |
114 | + ? this.timewindow.history.historyType : HistoryWindowType.LAST_INTERVAL | |
115 | + ], | |
116 | + timewindowMs: [ | |
117 | + this.timewindow.history && typeof this.timewindow.history.timewindowMs !== 'undefined' | |
118 | + ? this.timewindow.history.timewindowMs : null | |
119 | + ], | |
120 | + interval: [ | |
121 | + this.timewindow.history && typeof this.timewindow.history.interval !== 'undefined' | |
122 | + ? this.timewindow.history.interval : null | |
123 | + ], | |
124 | + fixedTimewindow: [ | |
125 | + this.timewindow.history && typeof this.timewindow.history.fixedTimewindow !== 'undefined' | |
126 | + ? this.timewindow.history.fixedTimewindow : null | |
127 | + ] | |
128 | + } | |
129 | + ), | |
130 | + aggregation: this.fb.group( | |
131 | + { | |
132 | + type: [ | |
133 | + this.timewindow.aggregation && typeof this.timewindow.aggregation.type !== 'undefined' | |
134 | + ? this.timewindow.aggregation.type : null | |
135 | + ], | |
136 | + limit: [ | |
137 | + this.timewindow.aggregation && typeof this.timewindow.aggregation.limit !== 'undefined' | |
138 | + ? this.timewindow.aggregation.limit : null, | |
139 | + [Validators.min(this.minDatapointsLimit()), Validators.max(this.maxDatapointsLimit())] | |
140 | + ] | |
141 | + } | |
142 | + ) | |
143 | + }); | |
144 | + } | |
145 | + | |
146 | + update() { | |
147 | + const timewindowFormValue = this.timewindowForm.value; | |
148 | + this.timewindow.realtime = new IntervalWindow(); | |
149 | + this.timewindow.realtime.timewindowMs = timewindowFormValue.realtime.timewindowMs; | |
150 | + this.timewindow.realtime.interval = timewindowFormValue.realtime.interval; | |
151 | + this.timewindow.history = new HistoryWindow(); | |
152 | + this.timewindow.history.historyType = timewindowFormValue.history.historyType; | |
153 | + this.timewindow.history.timewindowMs = timewindowFormValue.history.timewindowMs; | |
154 | + this.timewindow.history.interval = timewindowFormValue.history.interval; | |
155 | + this.timewindow.history.fixedTimewindow = timewindowFormValue.history.fixedTimewindow; | |
156 | + if (this.aggregation) { | |
157 | + this.timewindow.aggregation = new Aggregation(); | |
158 | + this.timewindow.aggregation.type = timewindowFormValue.aggregation.type; | |
159 | + this.timewindow.aggregation.limit = timewindowFormValue.aggregation.limit; | |
160 | + } | |
161 | + this.result = this.timewindow; | |
162 | + this.overlayRef.dispose(); | |
163 | + } | |
164 | + | |
165 | + cancel() { | |
166 | + this.overlayRef.dispose(); | |
167 | + } | |
168 | + | |
169 | + minDatapointsLimit() { | |
170 | + return this.timeService.getMinDatapointsLimit(); | |
171 | + } | |
172 | + | |
173 | + maxDatapointsLimit() { | |
174 | + return this.timeService.getMaxDatapointsLimit(); | |
175 | + } | |
176 | + | |
177 | + minRealtimeAggInterval() { | |
178 | + return this.timeService.minIntervalLimit(this.timewindowForm.get('realtime').get('timewindowMs').value); | |
179 | + } | |
180 | + | |
181 | + maxRealtimeAggInterval() { | |
182 | + return this.timeService.maxIntervalLimit(this.timewindowForm.get('realtime').get('timewindowMs').value); | |
183 | + } | |
184 | + | |
185 | + minHistoryAggInterval() { | |
186 | + return this.timeService.minIntervalLimit(this.currentHistoryTimewindow()); | |
187 | + } | |
188 | + | |
189 | + maxHistoryAggInterval() { | |
190 | + return this.timeService.maxIntervalLimit(this.currentHistoryTimewindow()); | |
191 | + } | |
192 | + | |
193 | + currentHistoryTimewindow() { | |
194 | + const timewindowFormValue = this.timewindowForm.value; | |
195 | + if (timewindowFormValue.history.historyType === HistoryWindowType.LAST_INTERVAL) { | |
196 | + return timewindowFormValue.history.timewindowMs; | |
197 | + } else { | |
198 | + return timewindowFormValue.history.fixedTimewindow.endTimeMs - | |
199 | + timewindowFormValue.history.fixedTimewindow.startTimeMs; | |
200 | + } | |
201 | + } | |
202 | + | |
203 | +} | ... | ... |
1 | +<!-- | |
2 | + | |
3 | + Copyright © 2016-2019 The Thingsboard Authors | |
4 | + | |
5 | + Licensed under the Apache License, Version 2.0 (the "License"); | |
6 | + you may not use this file except in compliance with the License. | |
7 | + You may obtain a copy of the License at | |
8 | + | |
9 | + http://www.apache.org/licenses/LICENSE-2.0 | |
10 | + | |
11 | + Unless required by applicable law or agreed to in writing, software | |
12 | + distributed under the License is distributed on an "AS IS" BASIS, | |
13 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
14 | + See the License for the specific language governing permissions and | |
15 | + limitations under the License. | |
16 | + | |
17 | +--> | |
18 | +<button *ngIf="asButton" cdkOverlayOrigin #timewindowPanelOrigin="cdkOverlayOrigin" [disabled]="disabled" | |
19 | + mat-raised-button color="primary" (click)="openEditMode($event)"> | |
20 | + <mat-icon class="material-icons">query_builder</mat-icon> | |
21 | + <span>{{innerValue.displayValue}}</span> | |
22 | +</button> | |
23 | +<section *ngIf="!asButton" cdkOverlayOrigin #timewindowPanelOrigin="cdkOverlayOrigin" | |
24 | + class="tb-timewindow" fxLayout="row" fxLayoutAlign="start center"> | |
25 | + <button *ngIf="direction === 'left'" [disabled]="disabled" mat-button mat-icon-button class="tb-mat-32" | |
26 | + (click)="openEditMode($event)" | |
27 | + matTooltip="{{ 'timewindow.edit' | translate }}" | |
28 | + [matTooltipPosition]="tooltipPosition"> | |
29 | + <mat-icon class="material-icons">query_builder</mat-icon> | |
30 | + </button> | |
31 | + <span [fxHide]="hideLabel()" | |
32 | + (click)="openEditMode($event)" | |
33 | + matTooltip="{{ 'timewindow.edit' | translate }}" | |
34 | + [matTooltipPosition]="tooltipPosition"> | |
35 | + {{innerValue.displayValue}} | |
36 | + </span> | |
37 | + <button *ngIf="direction === 'right'" [disabled]="disabled" mat-button mat-icon-button class="tb-mat-32" | |
38 | + (click)="openEditMode($event)" | |
39 | + matTooltip="{{ 'timewindow.edit' | translate }}" | |
40 | + [matTooltipPosition]="tooltipPosition"> | |
41 | + <mat-icon class="material-icons">query_builder</mat-icon> | |
42 | + </button> | |
43 | +</section> | ... | ... |
1 | +/** | |
2 | + * Copyright © 2016-2019 The Thingsboard Authors | |
3 | + * | |
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + * you may not use this file except in compliance with the License. | |
6 | + * You may obtain a copy of the License at | |
7 | + * | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + * | |
10 | + * Unless required by applicable law or agreed to in writing, software | |
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + * See the License for the specific language governing permissions and | |
14 | + * limitations under the License. | |
15 | + */ | |
16 | +:host { | |
17 | + section.tb-timewindow { | |
18 | + span { | |
19 | + overflow: hidden; | |
20 | + text-overflow: ellipsis; | |
21 | + white-space: nowrap; | |
22 | + pointer-events: all; | |
23 | + cursor: pointer; | |
24 | + } | |
25 | + } | |
26 | +} | ... | ... |
1 | +/// | |
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | |
3 | +/// | |
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | +/// you may not use this file except in compliance with the License. | |
6 | +/// You may obtain a copy of the License at | |
7 | +/// | |
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | |
9 | +/// | |
10 | +/// Unless required by applicable law or agreed to in writing, software | |
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | |
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | +/// See the License for the specific language governing permissions and | |
14 | +/// limitations under the License. | |
15 | +/// | |
16 | + | |
17 | +import { | |
18 | + Component, | |
19 | + forwardRef, Inject, | |
20 | + Input, | |
21 | + OnDestroy, | |
22 | + OnInit, | |
23 | + ViewChild, | |
24 | + ViewContainerRef | |
25 | +} from '@angular/core'; | |
26 | +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; | |
27 | +import { TranslateService } from '@ngx-translate/core'; | |
28 | +import { MillisecondsToTimeStringPipe } from '@shared/pipe/milliseconds-to-time-string.pipe'; | |
29 | +import { | |
30 | + HistoryWindowType, | |
31 | + Timewindow, | |
32 | + TimewindowType | |
33 | +} from '@shared/models/time/time.models'; | |
34 | +import { DatePipe } from '@angular/common'; | |
35 | +import { | |
36 | + Overlay, | |
37 | + CdkOverlayOrigin, | |
38 | + OverlayConfig, | |
39 | + OverlayPositionBuilder, ConnectedPosition, PositionStrategy, OverlayRef | |
40 | +} from '@angular/cdk/overlay'; | |
41 | +import { | |
42 | + TIMEWINDOW_PANEL_DATA, | |
43 | + TimewindowPanelComponent, | |
44 | + TimewindowPanelData | |
45 | +} from '@shared/components/time/timewindow-panel.component'; | |
46 | +import { ComponentPortal, PortalInjector } from '@angular/cdk/portal'; | |
47 | +import { MediaBreakpoints } from '@shared/models/constants'; | |
48 | +import { BreakpointObserver } from '@angular/cdk/layout'; | |
49 | +import { DOCUMENT } from '@angular/common'; | |
50 | +import { WINDOW } from '@core/services/window.service'; | |
51 | +import { TimeService } from '@core/services/time.service'; | |
52 | +import { TooltipPosition } from '@angular/material/typings/tooltip'; | |
53 | + | |
54 | +@Component({ | |
55 | + selector: 'tb-timewindow', | |
56 | + templateUrl: './timewindow.component.html', | |
57 | + styleUrls: ['./timewindow.component.scss'], | |
58 | + providers: [ | |
59 | + { | |
60 | + provide: NG_VALUE_ACCESSOR, | |
61 | + useExisting: forwardRef(() => TimewindowComponent), | |
62 | + multi: true | |
63 | + } | |
64 | + ] | |
65 | +}) | |
66 | +export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAccessor { | |
67 | + | |
68 | + historyOnlyValue = false; | |
69 | + | |
70 | + @Input() | |
71 | + set historyOnly(val) { | |
72 | + this.historyOnlyValue = true; | |
73 | + } | |
74 | + | |
75 | + get historyOnly() { | |
76 | + return this.historyOnlyValue; | |
77 | + } | |
78 | + | |
79 | + aggregationValue = false; | |
80 | + | |
81 | + @Input() | |
82 | + set aggregation(val) { | |
83 | + this.aggregationValue = true; | |
84 | + } | |
85 | + | |
86 | + get aggregation() { | |
87 | + return this.aggregationValue; | |
88 | + } | |
89 | + | |
90 | + isToolbarValue = false; | |
91 | + | |
92 | + @Input() | |
93 | + set isToolbar(val) { | |
94 | + this.isToolbarValue = true; | |
95 | + } | |
96 | + | |
97 | + get isToolbar() { | |
98 | + return this.isToolbarValue; | |
99 | + } | |
100 | + | |
101 | + asButtonValue = false; | |
102 | + | |
103 | + @Input() | |
104 | + set asButton(val) { | |
105 | + this.asButtonValue = true; | |
106 | + } | |
107 | + | |
108 | + get asButton() { | |
109 | + return this.asButtonValue; | |
110 | + } | |
111 | + | |
112 | + @Input() | |
113 | + direction: 'left' | 'right' = 'left'; | |
114 | + | |
115 | + @Input() | |
116 | + tooltipPosition: TooltipPosition = 'above'; | |
117 | + | |
118 | + @Input() disabled: boolean; | |
119 | + | |
120 | + @ViewChild('timewindowPanelOrigin', {static: false}) timewindowPanelOrigin: CdkOverlayOrigin; | |
121 | + | |
122 | + innerValue: Timewindow; | |
123 | + | |
124 | + private propagateChange = (_: any) => {}; | |
125 | + | |
126 | + constructor(private translate: TranslateService, | |
127 | + private timeService: TimeService, | |
128 | + private millisecondsToTimeStringPipe: MillisecondsToTimeStringPipe, | |
129 | + private datePipe: DatePipe, | |
130 | + private overlay: Overlay, | |
131 | + public viewContainerRef: ViewContainerRef, | |
132 | + public breakpointObserver: BreakpointObserver, | |
133 | + @Inject(DOCUMENT) private document: Document, | |
134 | + @Inject(WINDOW) private window: Window) { | |
135 | + } | |
136 | + | |
137 | + ngOnInit(): void { | |
138 | + } | |
139 | + | |
140 | + ngOnDestroy(): void { | |
141 | + } | |
142 | + | |
143 | + openEditMode() { | |
144 | + if (this.disabled) { | |
145 | + return; | |
146 | + } | |
147 | + const isGtSm = this.breakpointObserver.isMatched(MediaBreakpoints['gt-sm']); | |
148 | + const position = this.overlay.position(); | |
149 | + const config = new OverlayConfig({ | |
150 | + panelClass: 'tb-timewindow-panel', | |
151 | + backdropClass: 'cdk-overlay-transparent-backdrop', | |
152 | + hasBackdrop: isGtSm, | |
153 | + }); | |
154 | + if (isGtSm) { | |
155 | + config.minWidth = '417px'; | |
156 | + config.maxHeight = '440px'; | |
157 | + const panelHeight = 375; | |
158 | + const panelWidth = 417; | |
159 | + const el = this.timewindowPanelOrigin.elementRef.nativeElement; | |
160 | + const offset = el.getBoundingClientRect(); | |
161 | + const scrollTop = this.window.pageYOffset || this.document.documentElement.scrollTop || this.document.body.scrollTop || 0; | |
162 | + const scrollLeft = this.window.pageXOffset || this.document.documentElement.scrollLeft || this.document.body.scrollLeft || 0; | |
163 | + const bottomY = offset.bottom - scrollTop; | |
164 | + const leftX = offset.left - scrollLeft; | |
165 | + let originX; | |
166 | + let originY; | |
167 | + let overlayX; | |
168 | + let overlayY; | |
169 | + const wHeight = this.document.documentElement.clientHeight; | |
170 | + const wWidth = this.document.documentElement.clientWidth; | |
171 | + if (bottomY + panelHeight > wHeight) { | |
172 | + originY = 'top'; | |
173 | + overlayY = 'bottom'; | |
174 | + } else { | |
175 | + originY = 'bottom'; | |
176 | + overlayY = 'top'; | |
177 | + } | |
178 | + if (leftX + panelWidth > wWidth) { | |
179 | + originX = 'end'; | |
180 | + overlayX = 'end'; | |
181 | + } else { | |
182 | + originX = 'start'; | |
183 | + overlayX = 'start'; | |
184 | + } | |
185 | + const connectedPosition: ConnectedPosition = { | |
186 | + originX, | |
187 | + originY, | |
188 | + overlayX, | |
189 | + overlayY | |
190 | + }; | |
191 | + config.positionStrategy = position.flexibleConnectedTo(this.timewindowPanelOrigin.elementRef) | |
192 | + .withPositions([connectedPosition]); | |
193 | + } else { | |
194 | + config.minWidth = '100%'; | |
195 | + config.minHeight = '100%'; | |
196 | + config.positionStrategy = position.global().top('0%').left('0%') | |
197 | + .right('0%').bottom('0%'); | |
198 | + } | |
199 | + | |
200 | + const overlayRef = this.overlay.create(config); | |
201 | + | |
202 | + overlayRef.backdropClick().subscribe(() => { | |
203 | + overlayRef.dispose(); | |
204 | + }); | |
205 | + | |
206 | + const injector = this._createTimewindowPanelInjector( | |
207 | + overlayRef, | |
208 | + { | |
209 | + timewindow: this.innerValue.clone(), | |
210 | + historyOnly: this.historyOnly, | |
211 | + aggregation: this.aggregation | |
212 | + } | |
213 | + ); | |
214 | + | |
215 | + const componentRef = overlayRef.attach(new ComponentPortal(TimewindowPanelComponent, this.viewContainerRef, injector)); | |
216 | + componentRef.onDestroy(() => { | |
217 | + if (componentRef.instance.result) { | |
218 | + this.innerValue = componentRef.instance.result; | |
219 | + this.updateDisplayValue(); | |
220 | + this.notifyChanged(); | |
221 | + } | |
222 | + }); | |
223 | + } | |
224 | + | |
225 | + private _createTimewindowPanelInjector(overlayRef: OverlayRef, data: TimewindowPanelData): PortalInjector { | |
226 | + const injectionTokens = new WeakMap<any, any>([ | |
227 | + [TIMEWINDOW_PANEL_DATA, data], | |
228 | + [OverlayRef, overlayRef] | |
229 | + ]); | |
230 | + return new PortalInjector(this.viewContainerRef.injector, injectionTokens); | |
231 | + } | |
232 | + | |
233 | + registerOnChange(fn: any): void { | |
234 | + this.propagateChange = fn; | |
235 | + } | |
236 | + | |
237 | + registerOnTouched(fn: any): void { | |
238 | + } | |
239 | + | |
240 | + setDisabledState(isDisabled: boolean): void { | |
241 | + this.disabled = isDisabled; | |
242 | + } | |
243 | + | |
244 | + writeValue(obj: Timewindow): void { | |
245 | + this.innerValue = Timewindow.initModelFromDefaultTimewindow(obj, this.timeService); | |
246 | + this.updateDisplayValue(); | |
247 | + } | |
248 | + | |
249 | + notifyChanged() { | |
250 | + this.propagateChange(this.innerValue.cloneSelectedTimewindow()); | |
251 | + } | |
252 | + | |
253 | + updateDisplayValue() { | |
254 | + if (this.innerValue.selectedTab === TimewindowType.REALTIME && !this.historyOnly) { | |
255 | + this.innerValue.displayValue = this.translate.instant('timewindow.realtime') + ' - ' + | |
256 | + this.translate.instant('timewindow.last-prefix') + ' ' + | |
257 | + this.millisecondsToTimeStringPipe.transform(this.innerValue.realtime.timewindowMs); | |
258 | + } else { | |
259 | + this.innerValue.displayValue = !this.historyOnly ? (this.translate.instant('timewindow.history') + ' - ') : ''; | |
260 | + if (this.innerValue.history.historyType === HistoryWindowType.LAST_INTERVAL) { | |
261 | + this.innerValue.displayValue += this.translate.instant('timewindow.last-prefix') + ' ' + | |
262 | + this.millisecondsToTimeStringPipe.transform(this.innerValue.history.timewindowMs); | |
263 | + } else { | |
264 | + const startString = this.datePipe.transform(this.innerValue.history.fixedTimewindow.startTimeMs, 'yyyy-MM-dd HH:mm:ss'); | |
265 | + const endString = this.datePipe.transform(this.innerValue.history.fixedTimewindow.endTimeMs, 'yyyy-MM-dd HH:mm:ss'); | |
266 | + this.innerValue.displayValue += this.translate.instant('timewindow.period', {startTime: startString, endTime: endString}); | |
267 | + } | |
268 | + } | |
269 | + } | |
270 | + | |
271 | + hideLabel() { | |
272 | + return this.isToolbar && !this.breakpointObserver.isMatched(MediaBreakpoints['gt-md']); | |
273 | + } | |
274 | + | |
275 | +} | ... | ... |
1 | +/// | |
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | |
3 | +/// | |
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | +/// you may not use this file except in compliance with the License. | |
6 | +/// You may obtain a copy of the License at | |
7 | +/// | |
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | |
9 | +/// | |
10 | +/// Unless required by applicable law or agreed to in writing, software | |
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | |
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | +/// See the License for the specific language governing permissions and | |
14 | +/// limitations under the License. | |
15 | +/// | |
16 | + | |
17 | +import {BaseData} from '@shared/models/base-data'; | |
18 | +import {DashboardId} from '@shared/models/id/dashboard-id'; | |
19 | +import {TenantId} from '@shared/models/id/tenant-id'; | |
20 | +import {ShortCustomerInfo} from '@shared/models/customer.model'; | |
21 | + | |
22 | +export interface DashboardInfo extends BaseData<DashboardId> { | |
23 | + tenantId: TenantId; | |
24 | + title: string; | |
25 | + assignedCustomers: Array<ShortCustomerInfo>; | |
26 | +} | |
27 | + | |
28 | +export interface DashboardConfiguration { | |
29 | + widgets: Array<any>; | |
30 | + // TODO: | |
31 | +} | |
32 | + | |
33 | +export interface Dashboard extends DashboardInfo { | |
34 | + configuration: DashboardConfiguration; | |
35 | +} | ... | ... |
1 | +/// | |
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | |
3 | +/// | |
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | +/// you may not use this file except in compliance with the License. | |
6 | +/// You may obtain a copy of the License at | |
7 | +/// | |
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | |
9 | +/// | |
10 | +/// Unless required by applicable law or agreed to in writing, software | |
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | |
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | +/// See the License for the specific language governing permissions and | |
14 | +/// limitations under the License. | |
15 | +/// | |
16 | + | |
17 | +import { EntityId } from '@shared/models/id/entity-id'; | |
18 | +import { PageLink } from '@shared/models/page/page-link'; | |
19 | +import { BehaviorSubject, Observable, of, ReplaySubject } from 'rxjs'; | |
20 | +import { emptyPageData, PageData } from '@shared/models/page/page-data'; | |
21 | +import { BaseData, HasId } from '@shared/models/base-data'; | |
22 | +import { CollectionViewer, DataSource } from '@angular/cdk/typings/collections'; | |
23 | +import { catchError, map, take, tap } from 'rxjs/operators'; | |
24 | +import { SelectionModel } from '@angular/cdk/collections'; | |
25 | + | |
26 | +export type EntitiesFetchFunction<T extends BaseData<HasId>, P extends PageLink> = (pageLink: P) => Observable<PageData<T>>; | |
27 | + | |
28 | +export class EntitiesDataSource<T extends BaseData<HasId>, P extends PageLink = PageLink> implements DataSource<T> { | |
29 | + | |
30 | + private entitiesSubject = new BehaviorSubject<T[]>([]); | |
31 | + private pageDataSubject = new BehaviorSubject<PageData<T>>(emptyPageData<T>()); | |
32 | + | |
33 | + public pageData$ = this.pageDataSubject.asObservable(); | |
34 | + | |
35 | + public selection = new SelectionModel<T>(true, []); | |
36 | + | |
37 | + public currentEntity: T = null; | |
38 | + | |
39 | + constructor(private fetchFunction: EntitiesFetchFunction<T, P>) {} | |
40 | + | |
41 | + connect(collectionViewer: CollectionViewer): Observable<T[] | ReadonlyArray<T>> { | |
42 | + return this.entitiesSubject.asObservable(); | |
43 | + } | |
44 | + | |
45 | + disconnect(collectionViewer: CollectionViewer): void { | |
46 | + this.entitiesSubject.complete(); | |
47 | + this.pageDataSubject.complete(); | |
48 | + } | |
49 | + | |
50 | + loadEntities(pageLink: P): Observable<PageData<T>> { | |
51 | + const result = new ReplaySubject<PageData<T>>(); | |
52 | + this.fetchFunction(pageLink).pipe( | |
53 | + tap(() => { | |
54 | + this.selection.clear(); | |
55 | + }), | |
56 | + catchError(() => of(emptyPageData<T>())), | |
57 | + ).subscribe( | |
58 | + (pageData) => { | |
59 | + this.entitiesSubject.next(pageData.data); | |
60 | + this.pageDataSubject.next(pageData); | |
61 | + result.next(pageData); | |
62 | + } | |
63 | + ); | |
64 | + return result; | |
65 | + } | |
66 | + | |
67 | + isAllSelected(): Observable<boolean> { | |
68 | + const numSelected = this.selection.selected.length; | |
69 | + return this.entitiesSubject.pipe( | |
70 | + map((entities) => numSelected === entities.length) | |
71 | + ); | |
72 | + } | |
73 | + | |
74 | + isEmpty(): Observable<boolean> { | |
75 | + return this.entitiesSubject.pipe( | |
76 | + map((entities) => !entities.length) | |
77 | + ); | |
78 | + } | |
79 | + | |
80 | + total(): Observable<number> { | |
81 | + return this.pageDataSubject.pipe( | |
82 | + map((pageData) => pageData.totalElements) | |
83 | + ); | |
84 | + } | |
85 | + | |
86 | + toggleCurrentEntity(entity: T): boolean { | |
87 | + if (this.currentEntity !== entity) { | |
88 | + this.currentEntity = entity; | |
89 | + return true; | |
90 | + } else { | |
91 | + return false; | |
92 | + } | |
93 | + } | |
94 | + | |
95 | + isCurrentEntity(entity: T): boolean { | |
96 | + return (this.currentEntity && entity && this.currentEntity.id && entity.id) && | |
97 | + (this.currentEntity.id.id === entity.id.id); | |
98 | + } | |
99 | + | |
100 | + masterToggle() { | |
101 | + this.entitiesSubject.pipe( | |
102 | + tap((entities) => { | |
103 | + const numSelected = this.selection.selected.length; | |
104 | + if (numSelected === entities.length) { | |
105 | + this.selection.clear(); | |
106 | + } else { | |
107 | + entities.forEach(row => this.selection.select(row)); | |
108 | + } | |
109 | + }), | |
110 | + take(1) | |
111 | + ).subscribe(); | |
112 | + } | |
113 | +} | ... | ... |
1 | +/// | |
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | |
3 | +/// | |
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | +/// you may not use this file except in compliance with the License. | |
6 | +/// You may obtain a copy of the License at | |
7 | +/// | |
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | |
9 | +/// | |
10 | +/// Unless required by applicable law or agreed to in writing, software | |
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | |
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | +/// See the License for the specific language governing permissions and | |
14 | +/// limitations under the License. | |
15 | +/// | |
16 | + | |
17 | +import { EntityId } from './entity-id'; | |
18 | +import { EntityType } from '@shared/models/entity-type.models'; | |
19 | + | |
20 | +export class DashboardId implements EntityId { | |
21 | + entityType = EntityType.DASHBOARD; | |
22 | + id: string; | |
23 | + constructor(id: string) { | |
24 | + this.id = id; | |
25 | + } | |
26 | +} | ... | ... |
... | ... | @@ -33,3 +33,20 @@ export interface MailServerSettings { |
33 | 33 | username: string; |
34 | 34 | password: string; |
35 | 35 | } |
36 | + | |
37 | +export interface GeneralSettings { | |
38 | + baseUrl: string; | |
39 | +} | |
40 | + | |
41 | +export interface UserPasswordPolicy { | |
42 | + minimumLength: number; | |
43 | + minimumUppercaseLetters: number; | |
44 | + minimumLowercaseLetters: number; | |
45 | + minimumDigits: number; | |
46 | + minimumSpecialCharacters: number; | |
47 | + passwordExpirationPeriodDays: number; | |
48 | +} | |
49 | + | |
50 | +export interface SecuritySettings { | |
51 | + passwordPolicy: UserPasswordPolicy; | |
52 | +} | ... | ... |
1 | +/// | |
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | |
3 | +/// | |
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | +/// you may not use this file except in compliance with the License. | |
6 | +/// You may obtain a copy of the License at | |
7 | +/// | |
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | |
9 | +/// | |
10 | +/// Unless required by applicable law or agreed to in writing, software | |
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | |
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | +/// See the License for the specific language governing permissions and | |
14 | +/// limitations under the License. | |
15 | +/// | |
16 | + | |
17 | +import { Pipe, PipeTransform } from '@angular/core'; | |
18 | + | |
19 | +@Pipe({ | |
20 | + name: 'enumToArray' | |
21 | +}) | |
22 | +export class EnumToArrayPipe implements PipeTransform { | |
23 | + transform(data: object) { | |
24 | + const keys = Object.keys(data); | |
25 | + return keys.slice(keys.length / 2); | |
26 | + } | |
27 | +} | ... | ... |
ui-ngx/src/app/shared/pipe/highlight.pipe.ts
0 → 100644
1 | +/// | |
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | |
3 | +/// | |
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | +/// you may not use this file except in compliance with the License. | |
6 | +/// You may obtain a copy of the License at | |
7 | +/// | |
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | |
9 | +/// | |
10 | +/// Unless required by applicable law or agreed to in writing, software | |
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | |
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | +/// See the License for the specific language governing permissions and | |
14 | +/// limitations under the License. | |
15 | +/// | |
16 | + | |
17 | +import {Pipe, PipeTransform} from '@angular/core'; | |
18 | + | |
19 | +@Pipe({ name: 'highlight' }) | |
20 | +export class HighlightPipe implements PipeTransform { | |
21 | + transform(text: string, search): string { | |
22 | + const pattern = search | |
23 | + .replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'); | |
24 | + const regex = new RegExp('^' + pattern, 'i'); | |
25 | + | |
26 | + return search ? text.replace(regex, match => `<b>${match}</b>`) : text; | |
27 | + } | |
28 | +} | ... | ... |
1 | +/// | |
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | |
3 | +/// | |
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | +/// you may not use this file except in compliance with the License. | |
6 | +/// You may obtain a copy of the License at | |
7 | +/// | |
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | |
9 | +/// | |
10 | +/// Unless required by applicable law or agreed to in writing, software | |
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | |
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | +/// See the License for the specific language governing permissions and | |
14 | +/// limitations under the License. | |
15 | +/// | |
16 | + | |
17 | +import { Pipe, PipeTransform } from '@angular/core'; | |
18 | +import { TranslateService } from '@ngx-translate/core'; | |
19 | + | |
20 | +@Pipe({ | |
21 | + name: 'milliSecondsToTimeString' | |
22 | +}) | |
23 | +export class MillisecondsToTimeStringPipe implements PipeTransform { | |
24 | + | |
25 | + constructor(private translate: TranslateService) { | |
26 | + } | |
27 | + | |
28 | + transform(millseconds: number, args?: any): string { | |
29 | + let seconds = Math.floor(millseconds / 1000); | |
30 | + const days = Math.floor(seconds / 86400); | |
31 | + let hours = Math.floor((seconds % 86400) / 3600); | |
32 | + let minutes = Math.floor(((seconds % 86400) % 3600) / 60); | |
33 | + seconds = seconds % 60; | |
34 | + let timeString = ''; | |
35 | + if (days > 0) { | |
36 | + timeString += this.translate.instant('timewindow.days', {days}); | |
37 | + } | |
38 | + if (hours > 0) { | |
39 | + if (timeString.length === 0 && hours === 1) { | |
40 | + hours = 0; | |
41 | + } | |
42 | + timeString += this.translate.instant('timewindow.hours', {hours}); | |
43 | + } | |
44 | + if (minutes > 0) { | |
45 | + if (timeString.length === 0 && minutes === 1) { | |
46 | + minutes = 0; | |
47 | + } | |
48 | + timeString += this.translate.instant('timewindow.minutes', {minutes}); | |
49 | + } | |
50 | + if (seconds > 0) { | |
51 | + if (timeString.length === 0 && seconds === 1) { | |
52 | + seconds = 0; | |
53 | + } | |
54 | + timeString += this.translate.instant('timewindow.seconds', {seconds}); | |
55 | + } | |
56 | + return timeString; | |
57 | + } | |
58 | +} | ... | ... |
... | ... | @@ -58,39 +58,41 @@ import { NospacePipe } from './pipe/nospace.pipe'; |
58 | 58 | import { TranslateModule } from '@ngx-translate/core'; |
59 | 59 | import { TbCheckboxComponent } from '@shared/components/tb-checkbox.component'; |
60 | 60 | import { HelpComponent } from '@shared/components/help.component'; |
61 | -// import { EntitiesTableComponent } from '@shared/components/entity/entities-table.component'; | |
62 | -// import { AddEntityDialogComponent } from '@shared/components/entity/add-entity-dialog.component'; | |
63 | -// import { DetailsPanelComponent } from '@shared/components/details-panel.component'; | |
64 | -// import { EntityDetailsPanelComponent } from '@shared/components/entity/entity-details-panel.component'; | |
61 | +import { EntitiesTableComponent } from '@shared/components/entity/entities-table.component'; | |
62 | +import { AddEntityDialogComponent } from '@shared/components/entity/add-entity-dialog.component'; | |
63 | +import { DetailsPanelComponent } from '@shared/components/details-panel.component'; | |
64 | +import { EntityDetailsPanelComponent } from '@shared/components/entity/entity-details-panel.component'; | |
65 | 65 | import { TbAnchorComponent } from '@shared/components/tb-anchor.component'; |
66 | -// import { ContactComponent } from '@shared/components/contact.component'; | |
66 | +import { ContactComponent } from '@shared/components/contact.component'; | |
67 | 67 | // import { AuditLogDetailsDialogComponent } from '@shared/components/audit-log/audit-log-details-dialog.component'; |
68 | 68 | // import { AuditLogTableComponent } from '@shared/components/audit-log/audit-log-table.component'; |
69 | -// import { MillisecondsToTimeStringPipe } from '@shared/pipe/milliseconds-to-time-string.pipe'; | |
70 | -// import { TimewindowComponent } from '@shared/components/time/timewindow.component'; | |
69 | +import { MillisecondsToTimeStringPipe } from '@shared/pipe/milliseconds-to-time-string.pipe'; | |
70 | +import { TimewindowComponent } from '@shared/components/time/timewindow.component'; | |
71 | 71 | import { OverlayModule } from '@angular/cdk/overlay'; |
72 | -// import { TimewindowPanelComponent } from '@shared/components/time/timewindow-panel.component'; | |
73 | -// import { TimeintervalComponent } from '@shared/components/time/timeinterval.component'; | |
74 | -// import { DatetimePeriodComponent } from '@shared/components/time/datetime-period.component'; | |
75 | -// import { EnumToArrayPipe } from '@shared/pipe/enum-to-array.pipe'; | |
72 | +import { TimewindowPanelComponent } from '@shared/components/time/timewindow-panel.component'; | |
73 | +import { TimeintervalComponent } from '@shared/components/time/timeinterval.component'; | |
74 | +import { DatetimePeriodComponent } from '@shared/components/time/datetime-period.component'; | |
75 | +import { EnumToArrayPipe } from '@shared/pipe/enum-to-array.pipe'; | |
76 | 76 | import { ClipboardModule } from 'ngx-clipboard'; |
77 | 77 | // import { ValueInputComponent } from '@shared/components/value-input.component'; |
78 | -// import { IntervalCountPipe } from '@shared/pipe/interval-count.pipe'; | |
79 | 78 | import { FullscreenDirective } from '@shared/components/fullscreen.directive'; |
79 | +import { HighlightPipe } from '@shared/pipe/highlight.pipe'; | |
80 | +import {DashboardAutocompleteComponent} from '@shared/components/dashboard-autocomplete.component'; | |
80 | 81 | |
81 | 82 | @NgModule({ |
82 | 83 | providers: [ |
83 | 84 | DatePipe, |
84 | -// MillisecondsToTimeStringPipe, | |
85 | -// EnumToArrayPipe, | |
85 | + MillisecondsToTimeStringPipe, | |
86 | + EnumToArrayPipe, | |
87 | + HighlightPipe | |
86 | 88 | // IntervalCountPipe, |
87 | 89 | ], |
88 | 90 | entryComponents: [ |
89 | 91 | TbSnackBarComponent, |
90 | 92 | TbAnchorComponent, |
91 | -// AddEntityDialogComponent, | |
93 | + AddEntityDialogComponent, | |
92 | 94 | // AuditLogDetailsDialogComponent, |
93 | -// TimewindowPanelComponent, | |
95 | + TimewindowPanelComponent, | |
94 | 96 | ], |
95 | 97 | declarations: [ |
96 | 98 | FooterComponent, |
... | ... | @@ -103,22 +105,23 @@ import { FullscreenDirective } from '@shared/components/fullscreen.directive'; |
103 | 105 | TbSnackBarComponent, |
104 | 106 | BreadcrumbComponent, |
105 | 107 | UserMenuComponent, |
106 | -// EntitiesTableComponent, | |
107 | -// AddEntityDialogComponent, | |
108 | -// DetailsPanelComponent, | |
109 | -// EntityDetailsPanelComponent, | |
110 | -// ContactComponent, | |
108 | + EntitiesTableComponent, | |
109 | + AddEntityDialogComponent, | |
110 | + DetailsPanelComponent, | |
111 | + EntityDetailsPanelComponent, | |
112 | + ContactComponent, | |
111 | 113 | // AuditLogTableComponent, |
112 | 114 | // AuditLogDetailsDialogComponent, |
113 | -// TimewindowComponent, | |
114 | -// TimewindowPanelComponent, | |
115 | -// TimeintervalComponent, | |
116 | -// DatetimePeriodComponent, | |
115 | + TimewindowComponent, | |
116 | + TimewindowPanelComponent, | |
117 | + TimeintervalComponent, | |
118 | + DatetimePeriodComponent, | |
117 | 119 | // ValueInputComponent, |
120 | + DashboardAutocompleteComponent, | |
118 | 121 | NospacePipe, |
119 | -// MillisecondsToTimeStringPipe, | |
120 | -// EnumToArrayPipe, | |
121 | -// IntervalCountPipe | |
122 | + MillisecondsToTimeStringPipe, | |
123 | + EnumToArrayPipe, | |
124 | + HighlightPipe | |
122 | 125 | ], |
123 | 126 | imports: [ |
124 | 127 | CommonModule, |
... | ... | @@ -169,16 +172,17 @@ import { FullscreenDirective } from '@shared/components/fullscreen.directive'; |
169 | 172 | TbCheckboxComponent, |
170 | 173 | BreadcrumbComponent, |
171 | 174 | UserMenuComponent, |
172 | -// EntitiesTableComponent, | |
173 | -// AddEntityDialogComponent, | |
174 | -// DetailsPanelComponent, | |
175 | -// EntityDetailsPanelComponent, | |
176 | -// ContactComponent, | |
175 | + EntitiesTableComponent, | |
176 | + AddEntityDialogComponent, | |
177 | + DetailsPanelComponent, | |
178 | + EntityDetailsPanelComponent, | |
179 | + ContactComponent, | |
177 | 180 | // AuditLogTableComponent, |
178 | -// TimewindowComponent, | |
179 | -// TimewindowPanelComponent, | |
180 | -// TimeintervalComponent, | |
181 | -// DatetimePeriodComponent, | |
181 | + TimewindowComponent, | |
182 | + TimewindowPanelComponent, | |
183 | + TimeintervalComponent, | |
184 | + DatetimePeriodComponent, | |
185 | + DashboardAutocompleteComponent, | |
182 | 186 | // ValueInputComponent, |
183 | 187 | MatButtonModule, |
184 | 188 | MatCheckboxModule, |
... | ... | @@ -215,9 +219,9 @@ import { FullscreenDirective } from '@shared/components/fullscreen.directive'; |
215 | 219 | ReactiveFormsModule, |
216 | 220 | OverlayModule, |
217 | 221 | NospacePipe, |
218 | -// MillisecondsToTimeStringPipe, | |
219 | -// EnumToArrayPipe, | |
220 | -// IntervalCountPipe, | |
222 | + MillisecondsToTimeStringPipe, | |
223 | + EnumToArrayPipe, | |
224 | + HighlightPipe, | |
221 | 225 | TranslateModule |
222 | 226 | ] |
223 | 227 | }) | ... | ... |
... | ... | @@ -590,6 +590,7 @@ |
590 | 590 | "add-datasource-prompt": "Please add datasource" |
591 | 591 | }, |
592 | 592 | "details": { |
593 | + "details": "Details", | |
593 | 594 | "edit-mode": "Edit mode", |
594 | 595 | "toggle-edit-mode": "Toggle edit mode" |
595 | 596 | }, |
... | ... | @@ -1378,6 +1379,7 @@ |
1378 | 1379 | "delete-tenants-title": "Are you sure you want to delete { count, plural, 1 {1 tenant} other {# tenants} }?", |
1379 | 1380 | "delete-tenants-action-title": "Delete { count, plural, 1 {1 tenant} other {# tenants} }", |
1380 | 1381 | "delete-tenants-text": "Be careful, after the confirmation all selected tenants will be removed and all related data will become unrecoverable.", |
1382 | + "created-time": "Created time", | |
1381 | 1383 | "title": "Title", |
1382 | 1384 | "title-required": "Title is required.", |
1383 | 1385 | "description": "Description", |
... | ... | @@ -1437,6 +1439,7 @@ |
1437 | 1439 | "delete-users-text": "Be careful, after the confirmation all selected users will be removed and all related data will become unrecoverable.", |
1438 | 1440 | "activation-email-sent-message": "Activation email was successfully sent!", |
1439 | 1441 | "resend-activation": "Resend activation", |
1442 | + "created-time": "Created time", | |
1440 | 1443 | "email": "Email", |
1441 | 1444 | "email-required": "Email is required.", |
1442 | 1445 | "invalid-email-format": "Invalid email format.", | ... | ... |
... | ... | @@ -152,7 +152,7 @@ |
152 | 152 | "aknowledge-alarm-title": "Reconocer alarma", |
153 | 153 | "aknowledge-alarm-text": "¿Está seguro que quiere reconocer la alarma?", |
154 | 154 | "clear-alarms-title": "Quitar { count, plural, 1 {1 alarma} other {# alarmas} }", |
155 | - "clear-alarms-text": "¿Está seguro de que desea quitar { count, plural, 1 {1 alarma} other {# alarmas}?", | |
155 | + "clear-alarms-text": "¿Está seguro de que desea quitar { count, plural, 1 {1 alarma} other {# alarmas} }?", | |
156 | 156 | "clear-alarm-title": "Quitar alarma", |
157 | 157 | "clear-alarm-text": "¿Está seguro que quiere quitar la alarma?", |
158 | 158 | "alarm-status-filter": "Filtro de estado de alarma" | ... | ... |
... | ... | @@ -88,16 +88,16 @@ |
88 | 88 | "alarm": { |
89 | 89 | "ack-time": "Heure d'acquittement", |
90 | 90 | "acknowledge": "Acquitter", |
91 | - "aknowledge-alarms-text": "Etes-vous sûr de vouloir acquitter {count, plural, 1 {1 alarme} other {# alarmes}}?", | |
92 | - "aknowledge-alarms-title": "Acquitter {count, plural, 1 {1 alarme} other {# alarmes}}", | |
91 | + "aknowledge-alarms-text": "Etes-vous sûr de vouloir acquitter { count, plural, 1 {1 alarme} other {# alarmes} }?", | |
92 | + "aknowledge-alarms-title": "Acquitter { count, plural, 1 {1 alarme} other {# alarmes} }", | |
93 | 93 | "alarm": "Alarme", |
94 | 94 | "alarm-details": "Détails de l'alarme", |
95 | 95 | "alarm-required": "Une alarme est requise", |
96 | 96 | "alarm-status": "Etat d'alarme", |
97 | 97 | "alarms": "Alarmes", |
98 | 98 | "clear": "Effacer", |
99 | - "clear-alarms-text": "Êtes-vous sûr de vouloir effacer {count, plural, 1 {1 alarme} other {# alarmes}}?", | |
100 | - "clear-alarms-title": "Effacer {count, plural, 1 {1 alarme} other {# alarmes}}", | |
99 | + "clear-alarms-text": "Êtes-vous sûr de vouloir effacer { count, plural, 1 {1 alarme} other {# alarmes} }?", | |
100 | + "clear-alarms-title": "Effacer { count, plural, 1 {1 alarme} other {# alarmes} }", | |
101 | 101 | "clear-time": "Heure d'éffacement", |
102 | 102 | "created-time": "Heure de création", |
103 | 103 | "details": "Détails", |
... | ... | @@ -125,7 +125,7 @@ |
125 | 125 | "UNACK": "non acquittée" |
126 | 126 | }, |
127 | 127 | "select-alarm": "Sélectionnez une alarme", |
128 | - "selected-alarms": "{count, plural, 1 {1 alarme} other {# alarmes}} sélectionnées", | |
128 | + "selected-alarms": "{ count, plural, 1 {1 alarme} other {# alarmes} } sélectionnées", | |
129 | 129 | "severity": "Gravitée", |
130 | 130 | "severity-critical": "Critique", |
131 | 131 | "severity-indeterminate": "indéterminée", |
... | ... | @@ -192,7 +192,7 @@ |
192 | 192 | "assign-asset-to-customer": "Attribuer des Assets au client", |
193 | 193 | "assign-asset-to-customer-text": "Veuillez sélectionner les Assets à attribuer au client", |
194 | 194 | "assign-assets": "Attribuer des Assets", |
195 | - "assign-assets-text": "Attribuer {count, plural, 1 {1 asset} other {# assets}} au client", | |
195 | + "assign-assets-text": "Attribuer { count, plural, 1 {1 asset} other {# assets} } au client", | |
196 | 196 | "assign-new-asset": "Attribuer un nouvel Asset", |
197 | 197 | "assign-to-customer": "Attribuer au client", |
198 | 198 | "assign-to-customer-text": "Veuillez sélectionner le client pour attribuer le ou les Assets", |
... | ... | @@ -202,9 +202,9 @@ |
202 | 202 | "delete-asset-text": "Faites attention, après la confirmation, l'Asset et toutes les données associées deviendront irrécupérables.", |
203 | 203 | "delete-asset-title": "Êtes-vous sûr de vouloir supprimer l'Asset '{{assetName}}'?", |
204 | 204 | "delete-assets": "Supprimer des Assets", |
205 | - "delete-assets-action-title": "Supprimer {count, plural, 1 {1 asset} other {# assets}}", | |
205 | + "delete-assets-action-title": "Supprimer { count, plural, 1 {1 asset} other {# assets} }", | |
206 | 206 | "delete-assets-text": "Attention, après la confirmation, tous les Assets sélectionnés seront supprimés et toutes les données associées deviendront irrécupérables.", |
207 | - "delete-assets-title": "Etes-vous sûr de vouloir supprimer {count, plural, 1 {1 asset} other {# assets}}?", | |
207 | + "delete-assets-title": "Etes-vous sûr de vouloir supprimer { count, plural, 1 {1 asset} other {# assets} }?", | |
208 | 208 | "description": "Description", |
209 | 209 | "details": "Détails", |
210 | 210 | "enter-asset-type": "Entrez le type d'Asset", |
... | ... | @@ -232,9 +232,9 @@ |
232 | 232 | "unassign-asset-text": "Après la confirmation, l'Asset sera non attribué et ne sera pas accessible au client.", |
233 | 233 | "unassign-asset-title": "Êtes-vous sûr de vouloir retirer l'attribution de l'Asset '{{assetName}}'?", |
234 | 234 | "unassign-assets": "Retirer les Assets", |
235 | - "unassign-assets-action-title": "Retirer {count, plural, 1 {1 asset} other {# assets}} du client", | |
235 | + "unassign-assets-action-title": "Retirer { count, plural, 1 {1 asset} other {# assets} } du client", | |
236 | 236 | "unassign-assets-text": "Après la confirmation, tous les Assets sélectionnés ne seront pas attribués et ne seront pas accessibles au client.", |
237 | - "unassign-assets-title": "Êtes-vous sûr de vouloir retirer l'attribution de {count, plural, 1 {1 asset} other {# assets}}?", | |
237 | + "unassign-assets-title": "Êtes-vous sûr de vouloir retirer l'attribution de { count, plural, 1 {1 asset} other {# assets} }?", | |
238 | 238 | "unassign-from-customer": "Retirer du client", |
239 | 239 | "view-assets": "Afficher les Assets" |
240 | 240 | }, |
... | ... | @@ -246,7 +246,7 @@ |
246 | 246 | "attributes-scope": "Etendue des attributs d'entité", |
247 | 247 | "delete-attributes": "Supprimer les attributs", |
248 | 248 | "delete-attributes-text": "Attention, après la confirmation, tous les attributs sélectionnés seront supprimés.", |
249 | - "delete-attributes-title": "Êtes-vous sûr de vouloir supprimer {count, plural, 1 {1 attribut} other {# attributs}}?", | |
249 | + "delete-attributes-title": "Êtes-vous sûr de vouloir supprimer { count, plural, 1 {1 attribut} other {# attributs} }?", | |
250 | 250 | "enter-attribute-value": "Entrez la valeur de l'attribut", |
251 | 251 | "key": "Clé", |
252 | 252 | "key-required": "La Clé d'attribut est requise.", |
... | ... | @@ -258,8 +258,8 @@ |
258 | 258 | "scope-latest-telemetry": "Dernière télémétrie", |
259 | 259 | "scope-server": "Attributs du serveur", |
260 | 260 | "scope-shared": "Attributs partagés", |
261 | - "selected-attributes": "{count, plural, 1 {1 attribut} other {# attributs}} sélectionnés", | |
262 | - "selected-telemetry": "{count, plural, 1 {1 unité de télémétrie} other {# unités de télémétrie}} sélectionnées", | |
261 | + "selected-attributes": "{ count, plural, 1 {1 attribut} other {# attributs} } sélectionnés", | |
262 | + "selected-telemetry": "{ count, plural, 1 {1 unité de télémétrie} other {# unités de télémétrie} } sélectionnées", | |
263 | 263 | "show-on-widget": "Afficher sur le widget", |
264 | 264 | "value": "Valeur", |
265 | 265 | "value-required": "La valeur d'attribut est obligatoire.", |
... | ... | @@ -358,9 +358,9 @@ |
358 | 358 | "delete": "Supprimer le client", |
359 | 359 | "delete-customer-text": "Faites attention, après la confirmation, le client et toutes les données associées deviendront irrécupérables.", |
360 | 360 | "delete-customer-title": "Êtes-vous sûr de vouloir supprimer le client '{{customerTitle}}'?", |
361 | - "delete-customers-action-title": "Supprimer {count, plural, 1 {1 client} other {# clients}}", | |
361 | + "delete-customers-action-title": "Supprimer { count, plural, 1 {1 client} other {# clients} }", | |
362 | 362 | "delete-customers-text": "Faites attention, après la confirmation, tous les clients sélectionnés seront supprimés et toutes les données associées deviendront irrécupérables.", |
363 | - "delete-customers-title": "Êtes-vous sûr de vouloir supprimer {count, plural, 1 {1 client} other {# clients}}?", | |
363 | + "delete-customers-title": "Êtes-vous sûr de vouloir supprimer { count, plural, 1 {1 client} other {# clients} }?", | |
364 | 364 | "description": "Description", |
365 | 365 | "details": "Détails", |
366 | 366 | "devices": "Dispositifs du client", |
... | ... | @@ -397,7 +397,7 @@ |
397 | 397 | "assign-dashboard-to-customer": "Attribuer des tableaux de bord au client", |
398 | 398 | "assign-dashboard-to-customer-text": "Veuillez sélectionner les tableaux de bord à affecter au client", |
399 | 399 | "assign-dashboards": "Attribuer des tableaux de bord", |
400 | - "assign-dashboards-text": "Attribuer {count, plural, 1 {1 tableau de bord} other {# tableaux de bord}} aux clients", | |
400 | + "assign-dashboards-text": "Attribuer { count, plural, 1 {1 tableau de bord} other {# tableaux de bord} } aux clients", | |
401 | 401 | "assign-new-dashboard": "Attribuer un nouveau tableau de bord", |
402 | 402 | "assign-to-customer": "Attribuer au client", |
403 | 403 | "assign-to-customer-text": "Veuillez sélectionner le client pour attribuer le ou les tableaux de bord", |
... | ... | @@ -428,9 +428,9 @@ |
428 | 428 | "delete-dashboard-text": "Faites attention, après la confirmation, le tableau de bord et toutes les données associées deviendront irrécupérables.", |
429 | 429 | "delete-dashboard-title": "Êtes-vous sûr de vouloir supprimer le tableau de bord '{{dashboardTitle}}'?", |
430 | 430 | "delete-dashboards": "Supprimer les tableaux de bord", |
431 | - "delete-dashboards-action-title": "Supprimer {count, plural, 1 {1 tableau de bord} other {# tableaux de bord}}", | |
431 | + "delete-dashboards-action-title": "Supprimer {count, plural, 1 {1 tableau de bord} other {# tableaux de bord} }", | |
432 | 432 | "delete-dashboards-text": "Attention, après la confirmation, tous les tableaux de bord sélectionnés seront supprimés et toutes les données associées deviendront irrécupérables.", |
433 | - "delete-dashboards-title": "Voulez-vous vraiment supprimer {count, plural, 1 {1 tableau de bord} other {# tableaux de bord}}?", | |
433 | + "delete-dashboards-title": "Voulez-vous vraiment supprimer {count, plural, 1 {1 tableau de bord} other {# tableaux de bord} }?", | |
434 | 434 | "delete-state": "Supprimer l'état du tableau de bord", |
435 | 435 | "delete-state-text": "Etes-vous sûr de vouloir supprimer l'état du tableau de bord avec le nom '{{stateName}}'?", |
436 | 436 | "delete-state-title": "Supprimer l'état du tableau de bord", |
... | ... | @@ -493,7 +493,7 @@ |
493 | 493 | "select-state": "Sélectionnez l'état cible", |
494 | 494 | "select-widget-subtitle": "Liste des types de widgets disponibles", |
495 | 495 | "select-widget-title": "Sélectionner un widget", |
496 | - "selected-states": "{count, plural, 1 {1 état du tableau de bord} other {# états du tableau de bord}} sélectionnés", | |
496 | + "selected-states": "{count, plural, 1 {1 état du tableau de bord} other {# états du tableau de bord} } sélectionnés", | |
497 | 497 | "set-background": "Définir l'arrière-plan", |
498 | 498 | "settings": "Paramètres", |
499 | 499 | "show-details": "Afficher les détails", |
... | ... | @@ -515,10 +515,10 @@ |
515 | 515 | "unassign-dashboard-text": "Après la confirmation, le tableau de bord ne sera pas attribué et ne sera pas accessible au client.", |
516 | 516 | "unassign-dashboard-title": "Êtes-vous sûr de vouloir annuler l'affectation du tableau de bord '{{dashboardTitle}}'?", |
517 | 517 | "unassign-dashboards": "Retirer les tableaux de bord", |
518 | - "unassign-dashboards-action-text": "Annuler l'affectation {count, plural, 1 {1 tableau de bord} other {# tableaux de bord}} des clients", | |
519 | - "unassign-dashboards-action-title": "Annuler l'affectation {count, plural, 1 {1 tableau de bord} other {# tableaux de bord}} du client", | |
518 | + "unassign-dashboards-action-text": "Annuler l'affectation {count, plural, 1 {1 tableau de bord} other {# tableaux de bord} } des clients", | |
519 | + "unassign-dashboards-action-title": "Annuler l'affectation {count, plural, 1 {1 tableau de bord} other {# tableaux de bord} } du client", | |
520 | 520 | "unassign-dashboards-text": "Après la confirmation, tous les tableaux de bord sélectionnés ne seront pas attribués et ne seront pas accessibles au client.", |
521 | - "unassign-dashboards-title": "Etes-vous sûr de vouloir annuler l'affectation {count, plural, 1 {1 tableau de bord} other {# tableaux de bord}}?", | |
521 | + "unassign-dashboards-title": "Etes-vous sûr de vouloir annuler l'affectation {count, plural, 1 {1 tableau de bord} other {# tableaux de bord} }?", | |
522 | 522 | "unassign-from-customer": "Retirer du client", |
523 | 523 | "unassign-from-customers": "Retirer les tableaux de bord des clients", |
524 | 524 | "unassign-from-customers-text": "Veuillez sélectionner les clients à annuler l'affectation du ou des tableaux de bord", |
... | ... | @@ -541,8 +541,8 @@ |
541 | 541 | "function-types": "Types de fonctions", |
542 | 542 | "function-types-required": "Les types de fonctions sont obligatoires", |
543 | 543 | "label": "Label", |
544 | - "maximum-function-types": "Maximum {count, plural, 1 {1 type de fonction est autorisé.} other {# types de fonctions sont autorisés}}", | |
545 | - "maximum-timeseries-or-attributes": "Maximum {count, plural, 1 {1 timeseries / attribut est autorisé.} other {# timeseries / attributs sont autorisés}}", | |
544 | + "maximum-function-types": "Maximum {count, plural, 1 {1 type de fonction est autorisé.} other {# types de fonctions sont autorisés} }", | |
545 | + "maximum-timeseries-or-attributes": "Maximum {count, plural, 1 {1 timeseries / attribut est autorisé.} other {# timeseries / attributs sont autorisés} }", | |
546 | 546 | "settings": "Paramètres", |
547 | 547 | "timeseries": "Timeseries", |
548 | 548 | "timeseries-or-attributes-required": "Les timeseries / attributs d'entité sont obligatoires.", |
... | ... | @@ -580,7 +580,7 @@ |
580 | 580 | "assign-device-to-customer": "Affecter des dispositifs au client", |
581 | 581 | "assign-device-to-customer-text": "Veuillez sélectionner les dispositif à affecter au client", |
582 | 582 | "assign-devices": "Attribuer des dispositifs", |
583 | - "assign-devices-text": "Attribuer {count, plural, 1 {1 dispositif} other {# dispositifs}} au client", | |
583 | + "assign-devices-text": "Attribuer {count, plural, 1 {1 dispositif} other {# dispositifs} } au client", | |
584 | 584 | "assign-new-device": "Attribuer un nouveau dispositif", |
585 | 585 | "assign-to-customer": "Attribuer au client", |
586 | 586 | "assign-to-customer-text": "Veuillez sélectionner le client pour attribuer le ou les dispositifs", |
... | ... | @@ -596,9 +596,9 @@ |
596 | 596 | "delete-device-text": "Faites attention, après la confirmation, le dispositif et toutes les données associées deviendront irrécupérables.", |
597 | 597 | "delete-device-title": "Êtes-vous sûr de vouloir supprimer le dispositif '{{deviceName}}'?", |
598 | 598 | "delete-devices": "Supprimer les dispositifs", |
599 | - "delete-devices-action-title": "Supprimer {count, plural, 1 {1 dispositif} other {# dispositifs}}", | |
599 | + "delete-devices-action-title": "Supprimer {count, plural, 1 {1 dispositif} other {# dispositifs} }", | |
600 | 600 | "delete-devices-text": "Faites attention, après la confirmation, tous les dispositifs sélectionnés seront supprimés et toutes les données associées deviendront irrécupérables.", |
601 | - "delete-devices-title": "Êtes-vous sûr de vouloir supprimer {count, plural, 1 {1 dispositif} other {# dispositifs}}?", | |
601 | + "delete-devices-title": "Êtes-vous sûr de vouloir supprimer {count, plural, 1 {1 dispositif} other {# dispositifs} }?", | |
602 | 602 | "description": "Description", |
603 | 603 | "details": "Détails", |
604 | 604 | "device": "Dispositif", |
... | ... | @@ -653,9 +653,9 @@ |
653 | 653 | "unassign-device-text": "Après la confirmation, le dispositif ne sera pas attribué et ne sera pas accessible au client.", |
654 | 654 | "unassign-device-title": "Êtes-vous sûr de vouloir annuler l'affection du dispositif {{deviceName}} '?", |
655 | 655 | "unassign-devices": "Annuler l'affectation des dispositifs", |
656 | - "unassign-devices-action-title": "Annuler l'affectation de {count, plural, 1 {1 dispositif} other {#dispositifs}} du client", | |
656 | + "unassign-devices-action-title": "Annuler l'affectation de {count, plural, 1 {1 dispositif} other {#dispositifs} } du client", | |
657 | 657 | "unassign-devices-text": "Après la confirmation, tous les dispositifs sélectionnés ne seront pas attribues et ne seront pas accessibles par le client.", |
658 | - "unassign-devices-title": "Voulez-vous vraiment annuler l'affectation de {count, plural, 1 {1 dispositif} other {# dispositifs}}?", | |
658 | + "unassign-devices-title": "Voulez-vous vraiment annuler l'affectation de {count, plural, 1 {1 dispositif} other {# dispositifs} }?", | |
659 | 659 | "unassign-from-customer": "Retirer du client", |
660 | 660 | "use-device-name-filter": "Utiliser le filtre", |
661 | 661 | "view-credentials": "Afficher les informations d'identification", |
... | ... | @@ -696,17 +696,17 @@ |
696 | 696 | "entity-types": "Types d'entité", |
697 | 697 | "key": "Clé", |
698 | 698 | "key-name": "Nom de la clé", |
699 | - "list-of-alarms": "{count, plural, 1 {Une alarme} other {Liste de # alarmes}}", | |
700 | - "list-of-assets": "{count, plural, 1 {Un Asset} other {Liste de # Assets}}", | |
701 | - "list-of-customers": "{count, plural, 1 {Un client} other {Liste de # clients}}", | |
702 | - "list-of-dashboards": "{count, plural, 1 {Un tableau de bord} other {Liste de # tableaux de bord}}", | |
703 | - "list-of-devices": "{count, plural, 1 {Un dispositif} other {Liste de # dispositifs}}", | |
704 | - "list-of-plugins": "{count, plural, 1 {Un plugin} other {Liste de # plugins}}", | |
705 | - "list-of-rulechains": "{count, plural, 1 {Une chaîne de règles} other {Liste de # chaînes de règles}}", | |
706 | - "list-of-rulenodes": "{count, plural, 1 {Un noeud de règles} other {Liste de # noeuds de règles}}", | |
707 | - "list-of-rules": "{count, plural, 1 {Une règle} other {Liste de # règles}}", | |
708 | - "list-of-tenants": "{count, plural, 1 {Un tenant} other {Liste de # tenants}}", | |
709 | - "list-of-users": "{count, plural, 1 {Un utilisateur} other {Liste de # utilisateurs}}", | |
699 | + "list-of-alarms": "{count, plural, 1 {Une alarme} other {Liste de # alarmes} }", | |
700 | + "list-of-assets": "{count, plural, 1 {Un Asset} other {Liste de # Assets} }", | |
701 | + "list-of-customers": "{count, plural, 1 {Un client} other {Liste de # clients} }", | |
702 | + "list-of-dashboards": "{count, plural, 1 {Un tableau de bord} other {Liste de # tableaux de bord} }", | |
703 | + "list-of-devices": "{count, plural, 1 {Un dispositif} other {Liste de # dispositifs} }", | |
704 | + "list-of-plugins": "{count, plural, 1 {Un plugin} other {Liste de # plugins} }", | |
705 | + "list-of-rulechains": "{count, plural, 1 {Une chaîne de règles} other {Liste de # chaînes de règles} }", | |
706 | + "list-of-rulenodes": "{count, plural, 1 {Un noeud de règles} other {Liste de # noeuds de règles} }", | |
707 | + "list-of-rules": "{count, plural, 1 {Une règle} other {Liste de # règles} }", | |
708 | + "list-of-tenants": "{count, plural, 1 {Un tenant} other {Liste de # tenants} }", | |
709 | + "list-of-users": "{count, plural, 1 {Un utilisateur} other {Liste de # utilisateurs} }", | |
710 | 710 | "missing-entity-filter-error": "Le filtre est manquant pour l'alias '{{alias}}'.", |
711 | 711 | "name-starts-with": "Nom commence par", |
712 | 712 | "no-alias-matching": "'{{alias}}' introuvable.", |
... | ... | @@ -724,7 +724,7 @@ |
724 | 724 | "rulenode-name-starts-with": "Les noeuds de règles dont le nom commence par '{{prefix}}'", |
725 | 725 | "search": "Recherche d'entités", |
726 | 726 | "select-entities": "Sélectionner des entités", |
727 | - "selected-entities": "{count, plural, 1 {1 entité} other {# entités}} sélectionnées", | |
727 | + "selected-entities": "{count, plural, 1 {1 entité} other {# entités} } sélectionnées", | |
728 | 728 | "tenant-name-starts-with": "Les Tenant dont le nom commence par '{{prefix}}'", |
729 | 729 | "type": "Type", |
730 | 730 | "type-alarm": "Alarme", |
... | ... | @@ -832,7 +832,7 @@ |
832 | 832 | "delete-extension-text": "Attention, après la confirmation, l'extension et toutes les données associées deviendront irrécupérables.", |
833 | 833 | "delete-extension-title": "Êtes-vous sûr de vouloir supprimer l'extension '{{extensionId}}'?", |
834 | 834 | "delete-extensions-text": "Attention, après la confirmation, toutes les extensions sélectionnées seront supprimées.", |
835 | - "delete-extensions-title": "Êtes-vous sûr de vouloir supprimer {count, plural, 1 {1 extension} other {# extensions}}?", | |
835 | + "delete-extensions-title": "Êtes-vous sûr de vouloir supprimer {count, plural, 1 {1 extension} other {# extensions} }?", | |
836 | 836 | "device-name-expression": "expression du nom du dispositif", |
837 | 837 | "device-name-filter": "Filtre de nom de dispositif", |
838 | 838 | "device-type-expression": "expression de type de dispositif", |
... | ... | @@ -918,7 +918,7 @@ |
918 | 918 | "response-timeout": "Délai de réponse en millisecondes", |
919 | 919 | "response-topic-expression": "Expression du topic de la réponse", |
920 | 920 | "retry-interval": "Intervalle de nouvelle tentative en millisecondes", |
921 | - "selected-extensions": "{count, plural, 1 {1 extension} other {# extensions}} sélectionné", | |
921 | + "selected-extensions": "{count, plural, 1 {1 extension} other {# extensions} } sélectionné", | |
922 | 922 | "server-side-rpc": "RPC côté serveur", |
923 | 923 | "ssl": "Ssl", |
924 | 924 | "sync": { |
... | ... | @@ -960,9 +960,9 @@ |
960 | 960 | "delete-item-text": "Faites attention, après la confirmation, cet élément et toutes les données associées deviendront irrécupérables.", |
961 | 961 | "delete-item-title": "Êtes-vous sûr de vouloir supprimer cet élément?", |
962 | 962 | "delete-items": "Supprimer les éléments", |
963 | - "delete-items-action-title": "Supprimer {count, plural, 1 {1 élément} other {# éléments}}", | |
963 | + "delete-items-action-title": "Supprimer {count, plural, 1 {1 élément} other {# éléments} }", | |
964 | 964 | "delete-items-text": "Attention, après la confirmation, tous les éléments sélectionnés seront supprimés et toutes les données associées deviendront irrécupérables.", |
965 | - "delete-items-title": "Etes-vous sûr de vouloir supprimer {count, plural, 1 {1 élément} other {# éléments}}?", | |
965 | + "delete-items-title": "Etes-vous sûr de vouloir supprimer {count, plural, 1 {1 élément} other {# éléments} }?", | |
966 | 966 | "item-details": "Détails de l'élément", |
967 | 967 | "no-items-text": "Aucun élément trouvé", |
968 | 968 | "scroll-to-top": "Défiler vers le haut" |
... | ... | @@ -1080,11 +1080,11 @@ |
1080 | 1080 | "delete-from-relation-text": "Attention, après la confirmation, l'entité actuelle ne sera pas liée à l'entité '{{entityName}}'.", |
1081 | 1081 | "delete-from-relation-title": "Etes-vous sûr de vouloir supprimer la relation de l'entité '{{entityName}}'?", |
1082 | 1082 | "delete-from-relations-text": "Attention, après la confirmation, toutes les relations sélectionnées seront supprimées et l'entité actuelle ne sera pas liée aux entités correspondantes.", |
1083 | - "delete-from-relations-title": "Êtes-vous sûr de vouloir supprimer {count, plural, 1 {1 relation} other {# relations}}?", | |
1083 | + "delete-from-relations-title": "Êtes-vous sûr de vouloir supprimer {count, plural, 1 {1 relation} other {# relations} }?", | |
1084 | 1084 | "delete-to-relation-text": "Attention, après la confirmation, l'entité '{{entityName}} ne sera plus liée à l'entité actuelle.", |
1085 | 1085 | "delete-to-relation-title": "Êtes-vous sûr de vouloir supprimer la relation avec l'entité '{{entityName}}'?", |
1086 | 1086 | "delete-to-relations-text": "Attention, après la confirmation, toutes les relations sélectionnées seront supprimées et les entités correspondantes ne seront pas liées à l'entité en cours.", |
1087 | - "delete-to-relations-title": "Etes-vous sûr de vouloir supprimer {count, plural, 1 {1 relation} other {# relations}}?", | |
1087 | + "delete-to-relations-title": "Etes-vous sûr de vouloir supprimer {count, plural, 1 {1 relation} other {# relations} }?", | |
1088 | 1088 | "direction": "Sens", |
1089 | 1089 | "direction-type": { |
1090 | 1090 | "FROM": "de", |
... | ... | @@ -1105,7 +1105,7 @@ |
1105 | 1105 | "FROM": "De", |
1106 | 1106 | "TO": "À" |
1107 | 1107 | }, |
1108 | - "selected-relations": "{count, plural, 1 {1 relation} other {# relations}} sélectionné", | |
1108 | + "selected-relations": "{count, plural, 1 {1 relation} other {# relations} } sélectionné", | |
1109 | 1109 | "to-entity": "À l'entité", |
1110 | 1110 | "to-entity-name": "vers le nom de l'entité", |
1111 | 1111 | "to-entity-type": "Vers le type d'entité", |
... | ... | @@ -1121,9 +1121,9 @@ |
1121 | 1121 | "delete": "Supprimer la chaîne de règles", |
1122 | 1122 | "delete-rulechain-text": "Attention, après la confirmation, la chaîne de règles et toutes les données associées deviendront irrécupérables.", |
1123 | 1123 | "delete-rulechain-title": "Voulez-vous vraiment supprimer la chaîne de règles '{{ruleChainName}}'?", |
1124 | - "delete-rulechains-action-title": "Supprimer {count, plural, 1 {1 chaîne de règles} other {# chaînes de règles}}", | |
1124 | + "delete-rulechains-action-title": "Supprimer {count, plural, 1 {1 chaîne de règles} other {# chaînes de règles} }", | |
1125 | 1125 | "delete-rulechains-text": "Attention, après la confirmation, toutes les chaînes de règles sélectionnées seront supprimées et toutes les données associées deviendront irrécupérables.", |
1126 | - "delete-rulechains-title": "Êtes-vous sûr de vouloir supprimer {count, plural, 1 {1 chaîne de règles} other {# chaînes de règles}}?", | |
1126 | + "delete-rulechains-title": "Êtes-vous sûr de vouloir supprimer {count, plural, 1 {1 chaîne de règles} other {# chaînes de règles} }?", | |
1127 | 1127 | "description": "Description", |
1128 | 1128 | "details": "Détails", |
1129 | 1129 | "events": "Evénements", |
... | ... | @@ -1220,9 +1220,9 @@ |
1220 | 1220 | "delete": "Supprimer le Tenant", |
1221 | 1221 | "delete-tenant-text": "Attention, après la confirmation, le Tenant et toutes les données associées deviendront irrécupérables.", |
1222 | 1222 | "delete-tenant-title": "Etes-vous sûr de vouloir supprimer le tenant '{{tenantTitle}}'?", |
1223 | - "delete-tenants-action-title": "Supprimer {count, plural, 1 {1 tenant} other {# tenants}}", | |
1223 | + "delete-tenants-action-title": "Supprimer {count, plural, 1 {1 tenant} other {# tenants} }", | |
1224 | 1224 | "delete-tenants-text": "Attention, après la confirmation, tous les Tenants sélectionnés seront supprimés et toutes les données associées deviendront irrécupérables.", |
1225 | - "delete-tenants-title": "Etes-vous sûr de vouloir supprimer {count, plural, 1 {1 tenant} other {# tenants}}?", | |
1225 | + "delete-tenants-title": "Etes-vous sûr de vouloir supprimer {count, plural, 1 {1 tenant} other {# tenants} }?", | |
1226 | 1226 | "description": "Description", |
1227 | 1227 | "details": "Détails", |
1228 | 1228 | "events": "Événements", |
... | ... | @@ -1242,26 +1242,26 @@ |
1242 | 1242 | "timeinterval": { |
1243 | 1243 | "advanced": "Avancé", |
1244 | 1244 | "days": "Jours", |
1245 | - "days-interval": "{days, plural, 1 {1 jour} other {# jours}}", | |
1245 | + "days-interval": "{days, plural, 1 {1 jour} other {# jours} }", | |
1246 | 1246 | "hours": "Heures", |
1247 | - "hours-interval": "{hours, plural, 1 {1 heure} other {# heures}}", | |
1247 | + "hours-interval": "{hours, plural, 1 {1 heure} other {# heures} }", | |
1248 | 1248 | "minutes": "Minutes", |
1249 | - "minutes-interval": "{minutes, plural, 1 {1 minute} other {# minutes}}", | |
1249 | + "minutes-interval": "{minutes, plural, 1 {1 minute} other {# minutes} }", | |
1250 | 1250 | "seconds": "Secondes", |
1251 | - "seconds-interval": "{seconds, plural, 1 {1 seconde} other {# secondes}}" | |
1251 | + "seconds-interval": "{seconds, plural, 1 {1 seconde} other {# secondes} }" | |
1252 | 1252 | }, |
1253 | 1253 | "timewindow": { |
1254 | 1254 | "date-range": "Plage de dates", |
1255 | - "days": "{days, plural, 1 {jour} other {# jours}}", | |
1255 | + "days": "{days, plural, 1 {jour} other {# jours} }", | |
1256 | 1256 | "edit": "Modifier timewindow", |
1257 | 1257 | "history": "Historique", |
1258 | - "hours": "{hours, plural, 0 {heure} 1 {1 heure} other {# heures}}", | |
1258 | + "hours": "{hours, plural, 0 {heure} 1 {1 heure} other {# heures} }", | |
1259 | 1259 | "last": "Dernier", |
1260 | 1260 | "last-prefix": "dernier", |
1261 | - "minutes": "{minutes, plural, 0 {minute} 1 {1 minute} other {# minutes}}", | |
1261 | + "minutes": "{minutes, plural, 0 {minute} 1 {1 minute} other {# minutes} }", | |
1262 | 1262 | "period": "de {{startTime}} à {{endTime}}", |
1263 | 1263 | "realtime": "Temps réel", |
1264 | - "seconds": "{seconds, plural, 0 {second} 1 {1 second} other {# seconds}}", | |
1264 | + "seconds": "{seconds, plural, 0 {second} 1 {1 second} other {# seconds} }", | |
1265 | 1265 | "time-period": "Période" |
1266 | 1266 | }, |
1267 | 1267 | "user": { |
... | ... | @@ -1281,9 +1281,9 @@ |
1281 | 1281 | "delete": "Supprimer l'utilisateur", |
1282 | 1282 | "delete-user-text": "Attention, après la confirmation, l'utilisateur et toutes les données associées deviendront irrécupérables.", |
1283 | 1283 | "delete-user-title": "Etes-vous sûr de vouloir supprimer l'utilisateur '{{userEmail}}'?", |
1284 | - "delete-users-action-title": "Supprimer {count, plural, 1 {1 utilisateur} other {# utilisateurs}}", | |
1284 | + "delete-users-action-title": "Supprimer {count, plural, 1 {1 utilisateur} other {# utilisateurs} }", | |
1285 | 1285 | "delete-users-text": "Attention, après la confirmation, tous les utilisateurs sélectionnés seront supprimés et toutes les données associées deviendront irrécupérables.", |
1286 | - "delete-users-title": "Etes-vous sûr de vouloir supprimer {count, plural, 1 {1 utilisateur} other {# utilisateurs}}?", | |
1286 | + "delete-users-title": "Etes-vous sûr de vouloir supprimer {count, plural, 1 {1 utilisateur} other {# utilisateurs} }?", | |
1287 | 1287 | "description": "Description", |
1288 | 1288 | "details": "Détails", |
1289 | 1289 | "display-activation-link": "Afficher le lien d'activation", |
... | ... | @@ -1417,7 +1417,7 @@ |
1417 | 1417 | "general-settings": "Paramètres généraux", |
1418 | 1418 | "height": "Hauteur", |
1419 | 1419 | "margin": "Marge", |
1420 | - "maximum-datasources": "Maximum {count, plural, 1 {1 datasource est autorisé.} other {# datasources sont autorisés}}", | |
1420 | + "maximum-datasources": "Maximum {count, plural, 1 {1 datasource est autorisé.} other {# datasources sont autorisés} }", | |
1421 | 1421 | "mobile-mode-settings": "Paramètres du mode mobile", |
1422 | 1422 | "order": "Ordre", |
1423 | 1423 | "padding": "Padding", |
... | ... | @@ -1511,9 +1511,9 @@ |
1511 | 1511 | "delete": "Supprimer le groupe de widgets", |
1512 | 1512 | "delete-widgets-bundle-text": "Attention, après la confirmation, le groupe de widgets et toutes les données associées deviendront irrécupérables.", |
1513 | 1513 | "delete-widgets-bundle-title": "Êtes-vous sûr de vouloir supprimer le groupe de widgets '{{widgetsBundleTitle}}'?", |
1514 | - "delete-widgets-bundles-action-title": "Supprimer {count, plural, 1 {1 groupe de widgets} other {# groupes de widgets}}", | |
1514 | + "delete-widgets-bundles-action-title": "Supprimer {count, plural, 1 {1 groupe de widgets} other {# groupes de widgets} }", | |
1515 | 1515 | "delete-widgets-bundles-text": "Attention, après la confirmation, tous les groupes de widgets sélectionnés seront supprimés et toutes les données associées deviendront irrécupérables.", |
1516 | - "delete-widgets-bundles-title": "Voulez-vous vraiment supprimer {count, plural, 1 {1 groupe de widgets} other {# groupes de widgets}}?", | |
1516 | + "delete-widgets-bundles-title": "Voulez-vous vraiment supprimer {count, plural, 1 {1 groupe de widgets} other {# groupes de widgets} }?", | |
1517 | 1517 | "details": "Détails", |
1518 | 1518 | "empty": "Le groupe de widgets est vide", |
1519 | 1519 | "export": "Exporter le groupe de widgets", | ... | ... |
... | ... | @@ -626,7 +626,7 @@ |
626 | 626 | "delete-device-title": "Вы точно хотите удалить устройство '{{deviceName}}'?", |
627 | 627 | "delete-device-text": "Внимание, после подтверждения устройство и все связанные с ним данные будут безвозвратно утеряны.", |
628 | 628 | "delete-devices-title": "Вы точно хотите удалить { count, plural, one {1 устройство} few {# устройства} other {# устройств} }?", |
629 | - "delete-devices-action-title": "Удалить { count, plural, one {1 устройство} few {# устройства} other {# устройств} } }", | |
629 | + "delete-devices-action-title": "Удалить { count, plural, one {1 устройство} few {# устройства} other {# устройств} }", | |
630 | 630 | "delete-devices-text": "Внимание, после подтверждения выбранные устройства и все связанные с ними данные будут безвозвратно утеряны..", |
631 | 631 | "unassign-device-title": "Вы точно хотите отозвать устройство '{{deviceName}}'?", |
632 | 632 | "unassign-device-text": "После подтверждения устройство будет недоступно клиенту.", |
... | ... | @@ -1064,7 +1064,7 @@ |
1064 | 1064 | "delete-item-title": "Вы точно хотите удалить этот объект?", |
1065 | 1065 | "delete-item-text": "Внимание, после подтверждения объект и все связанные с ним данные будут безвозвратно утеряны.", |
1066 | 1066 | "delete-items-title": "Вы точно хотите удалить { count, plural, one {1 объект} few {# объекта} other {# объектов} }?", |
1067 | - "delete-items-action-title": "Удалить { count, plural, one {1 объект} few {# объекта} other {# объектов}", | |
1067 | + "delete-items-action-title": "Удалить { count, plural, one {1 объект} few {# объекта} other {# объектов} }", | |
1068 | 1068 | "delete-items-text": "Внимание, после подтверждения выбранные объекты и все связанные с ними данные будут безвозвратно утеряны.", |
1069 | 1069 | "add-item-text": "Добавить новый объект", |
1070 | 1070 | "no-items-text": "Объекты не найдены", |
... | ... | @@ -1646,4 +1646,4 @@ |
1646 | 1646 | "cs_CZ": "Чешский" |
1647 | 1647 | } |
1648 | 1648 | } |
1649 | -} | |
\ No newline at end of file | ||
1649 | +} | ... | ... |
... | ... | @@ -419,7 +419,7 @@ |
419 | 419 | "assign-dashboards": "Kontrol panelleri ata", |
420 | 420 | "assign-new-dashboard": "Yeni kontrol paneli ata", |
421 | 421 | "assign-dashboards-text": "{ count, plural, 1 {1 kontrol panelini} other {# kontrol panelini} } kullanıcı grubuna ata", |
422 | - "unassign-dashboards-action-text": "Müşterilerden atama { count, plural, 1 {1 gösterge tablosu} other {# panolar}}", | |
422 | + "unassign-dashboards-action-text": "Müşterilerden atama { count, plural, 1 {1 gösterge tablosu} other {# panolar} }", | |
423 | 423 | "delete-dashboards": "Kontrol panellerini sil", |
424 | 424 | "unassign-dashboards": "Kontrol panellerinden atamayı kaldır", |
425 | 425 | "unassign-dashboards-action-title": "{ count, plural, 1 {1 kontrol panelinin} other {# kontrol panelinin} } atamaları kullanıcı grubundan kaldır", |
... | ... | @@ -710,7 +710,7 @@ |
710 | 710 | "asset-name-starts-with": "İsmi '{{prefix}}' ile başlayan varlıklar", |
711 | 711 | "type-entity-view": "Varlık Görünümü", |
712 | 712 | "type-entity-views": "Varlık Görünümleri", |
713 | - "list-of-entity-views": "{ count, plural, 1 {Bir varlık görünümü} other {# varlık görüntüleme}} listesi", | |
713 | + "list-of-entity-views": "{ count, plural, 1 {Bir varlık görünümü} other {# varlık görüntüleme} } listesi", | |
714 | 714 | "entity-view-name-starts-with": "Adı {{önek}} ile başlayan varlık görünümleri", |
715 | 715 | "type-rule": "Kural", |
716 | 716 | "type-rules": "Kurallar", |
... | ... | @@ -742,11 +742,11 @@ |
742 | 742 | "alarm-name-starts-with": "İsmi '{{prefix}}' ile başlayan alarmlar", |
743 | 743 | "type-rulechain": "Kural zinciri", |
744 | 744 | "type-rulechains": "Kural zincirleri", |
745 | - "list-of-rulechains": "{ count, plural, 1 {Bir kural zinciri} other {# kural zincirinin listesi}}", | |
745 | + "list-of-rulechains": "{ count, plural, 1 {Bir kural zinciri} other {# kural zincirinin listesi} }", | |
746 | 746 | "rulechain-name-starts-with": "İsimleri {{prefix}} ile başlayan kural zincirleri", |
747 | 747 | "type-rulenode": "Kural düğümü", |
748 | 748 | "type-rulenodes": "Kural düğümleri", |
749 | - "list-of-rulenodes": "{ count, plural, 1 {Bir kural node} other {# kural düğümünün listesi}}", | |
749 | + "list-of-rulenodes": "{ count, plural, 1 {Bir kural node} other {# kural düğümünün listesi} }", | |
750 | 750 | "rulenode-name-starts-with": "İsimleri '{{prefix}} ile başlayan kural düğümleri", |
751 | 751 | "type-current-customer": "Mevcut Müşteri", |
752 | 752 | "search": "Öğeleri ara", |
... | ... | @@ -765,7 +765,7 @@ |
765 | 765 | "aliases": "Varlık Görünümü takma adları", |
766 | 766 | "no-alias-matching": "'{{alias}} bulunamadı. ", |
767 | 767 | "no-aliases-found": "Takma ad bulunamadı", |
768 | - "no-key-matching": "'{{anahtar bulunamadı.", | |
768 | + "no-key-matching": "'{{key}}' bulunamadı.", | |
769 | 769 | "no-keys-found": "Anahtar bulunamadı.", |
770 | 770 | "create-new-alias": "Yeni bir tane oluştur!", |
771 | 771 | "create-new-key": "Yeni bir tane oluştur!", |
... | ... | @@ -792,21 +792,21 @@ |
792 | 792 | "add-entity-view-text": "Yeni varlık görünümü ekle", |
793 | 793 | "delete": "Varlık görünümünü sil", |
794 | 794 | "assign-entity-views": "Varlık görünümleri atama", |
795 | - "assign-entity-views-text": "Müşteriye { count, plural, 1 {1 entityView} other {# entityViews}} atayın ", | |
795 | + "assign-entity-views-text": "Müşteriye { count, plural, 1 {1 entityView} other {# entityViews} } atayın ", | |
796 | 796 | "delete-entity-views": "Varlık görünümlerini sil", |
797 | 797 | "unassign-from-customer": "Müşteriden atama", |
798 | 798 | "unassign-entity-views": "Varlık görünümlerini atama", |
799 | - "unassign-entity-views-action-title": "Müşteriden atama { count, plural, 1 {1 entityView} other {# entityViews}}", | |
799 | + "unassign-entity-views-action-title": "Müşteriden atama { count, plural, 1 {1 entityView} other {# entityViews} }", | |
800 | 800 | "assign-new-entity-view": "Yeni varlık görünümü atama", |
801 | 801 | "delete-entity-view-title": "Varlık görünümünü silmek istediğinizden emin misiniz?, {{entityViewName}} '? ", |
802 | 802 | "delete-entity-view-text": "Dikkatli olun, onaylandıktan sonra varlık görünümü ve ilgili tüm veriler kurtarılamayacak.", |
803 | - "delete-entity-views-title": "{ count, plural, 1 {1 entityView} other {# entityViews}} varlık görünümüne sahip olmak istediğinizden emin misiniz?", | |
804 | - "delete-entity-views-action-title": "Sil { count, plural, 1 {1 entityView} other {# entityViews}}", | |
803 | + "delete-entity-views-title": "{ count, plural, 1 {1 entityView} other {# entityViews} } varlık görünümüne sahip olmak istediğinizden emin misiniz?", | |
804 | + "delete-entity-views-action-title": "Sil { count, plural, 1 {1 entityView} other {# entityViews} }", | |
805 | 805 | "delete-entity-views-text": "Dikkatli olun, onaylandıktan sonra tüm seçilen görünümler kaldırılacak ve ilgili tüm veriler kurtarılamayacaktır.", |
806 | 806 | "unassign-entity-view-title": "Varlık görünümünün atamasını kaldırmak istediğinizden emin misiniz? {{entityViewName}} '? ", |
807 | 807 | "unassign-entity-view-text": "Onaydan sonra varlık görünümü atanmamış olacak ve müşteri tarafından erişilemeyecektir.", |
808 | 808 | "unassign-entity-view": "Varlık görünümünün atamasını kaldır", |
809 | - "unassign-entity-views-title": "{ count, plural, 1 {1 entityView} other {# entityViews}} hesabının atamasını kaldırmak istediğinizden emin misiniz?", | |
809 | + "unassign-entity-views-title": "{ count, plural, 1 {1 entityView} other {# entityViews} } hesabının atamasını kaldırmak istediğinizden emin misiniz?", | |
810 | 810 | "unassign-entity-views-text": "Onaylandıktan sonra, seçilen tüm öğe görünümleri atamadan kaldırılacak ve müşteri tarafından erişilemeyecektir.", |
811 | 811 | "entity-view-type": "Varlık Görünümü türü", |
812 | 812 | "entity-view-type-required": "Varlık Görünümü türü gerekli.", |
... | ... | @@ -861,7 +861,7 @@ |
861 | 861 | }, |
862 | 862 | "extension": { |
863 | 863 | "extensions": "Uzantılar", |
864 | - "selected-extensions": "{ count, plural, 1 {1 uzantı} other {# extensions}} seçildi", | |
864 | + "selected-extensions": "{ count, plural, 1 {1 uzantı} other {# extensions} } seçildi", | |
865 | 865 | "type": "Tür", |
866 | 866 | "key": "Anahtar", |
867 | 867 | "value": "Değer", |
... | ... | @@ -875,7 +875,7 @@ |
875 | 875 | "edit": "Uzantıyı düzenle", |
876 | 876 | "delete-extension-title": "{{ExtensionId}} uzantısını silmek istediğinizden emin misiniz? ", |
877 | 877 | "delete-extension-text": "Dikkatli olun, onaylamadan sonra uzantı ve ilgili tüm veriler kurtarılamaz.", |
878 | - "delete-extensions-title": "{ count, plural, 1 {1 uzantı} other {# extensions}} silmek istediğinizden emin misiniz?", | |
878 | + "delete-extensions-title": "{ count, plural, 1 {1 uzantı} other {# extensions} } silmek istediğinizden emin misiniz?", | |
879 | 879 | "delete-extensions-text": "Dikkatli olun, onaylandıktan sonra tüm seçilen uzantılar kaldırılacak.", |
880 | 880 | "converters": "Dönüştürücü", |
881 | 881 | "converter-id": "Dönüştürücü kimliği", |
... | ... | @@ -1606,4 +1606,4 @@ |
1606 | 1606 | "cs_CZ": "Çekçe" |
1607 | 1607 | } |
1608 | 1608 | } |
1609 | -} | |
\ No newline at end of file | ||
1609 | +} | ... | ... |
... | ... | @@ -457,8 +457,8 @@ |
457 | 457 | "customer-details": "Інформація про клієнта", |
458 | 458 | "delete-customer-title": "Ви впевнені, що хочете видалити клієнта '{{customerTitle}}'?", |
459 | 459 | "delete-customer-text": "Будьте обережні, після підтвердження, клієнт та всі пов'язані з ним дані, стануть недоступними.", |
460 | - "delete-customers-title": "Ви впевнені, що хочете видалити {count, plural, 1 {1 клієнт}, інші {# клієнти}}?", | |
461 | - "delete-customers-action-title": "Видалити{ count, plural, 1 {1 клієнт} other {# клієнти} }", | |
460 | + "delete-customers-title": "Ви впевнені, що хочете видалити { count, plural, 1 {1 клієнт} other {# клієнти} }?", | |
461 | + "delete-customers-action-title": "Видалити { count, plural, 1 {1 клієнт} other {# клієнти} }", | |
462 | 462 | "delete-customers-text": "Будьте обережні, після підтвердження, всі вибрані клієнти будуть видалені і всі пов'язані з ними дані, стануть недоступними.", |
463 | 463 | "manage-users": "Керування користувачами", |
464 | 464 | "manage-assets": "Керування активами", |
... | ... | @@ -474,7 +474,7 @@ |
474 | 474 | "select-customer": "Виберіть клієнта", |
475 | 475 | "no-customers-matching": "Клієнтів, які відповідають '{{entity}}' не знайдено.", |
476 | 476 | "customer-required": "Необхідно задати клієнта", |
477 | - "selected-customers": "{ count, plural, 1 {1 клієнт} інші {# клієнти} } вибрано", | |
477 | + "selected-customers": "{ count, plural, 1 {1 клієнт} other {# клієнти} } вибрано", | |
478 | 478 | "search": "Пошук клієнтів", |
479 | 479 | "select-group-to-add": "Виберіть цільову групу, щоб додати вибраних клієнтів", |
480 | 480 | "select-group-to-move": "Виберіть цільову групу для переміщення вибраних клієнтів", | ... | ... |
... | ... | @@ -596,11 +596,11 @@ |
596 | 596 | "manage-credentials": "管理凭据", |
597 | 597 | "delete": "删除设备", |
598 | 598 | "assign-devices": "分配设备", |
599 | - "assign-devices-text": "将{count,plural,1 {1 设备} other {# 设备}}分配给客户", | |
599 | + "assign-devices-text": "将{count,plural,1 {1 设备} other {# 设备} }分配给客户", | |
600 | 600 | "delete-devices": "删除设备", |
601 | 601 | "unassign-from-customer": "取消分配客户", |
602 | 602 | "unassign-devices": "取消分配设备", |
603 | - "unassign-devices-action-title": "从客户处取消分配{count,plural,1 {1 设备} other {# 设备}}", | |
603 | + "unassign-devices-action-title": "从客户处取消分配{count,plural,1 {1 设备} other {# 设备} }", | |
604 | 604 | "assign-new-device": "分配新设备", |
605 | 605 | "make-public-device-title": "您确定要将设备 '{{deviceName}}' 设为公开吗?", |
606 | 606 | "make-public-device-text": "确认后,设备及其所有数据将被设为公开并可被其他人访问。", |
... | ... | @@ -609,13 +609,13 @@ |
609 | 609 | "view-credentials": "查看凭据", |
610 | 610 | "delete-device-title": "您确定要删除设备的{{deviceName}}吗?", |
611 | 611 | "delete-device-text": "小心!确认后设备及其所有相关数据将不可恢复。", |
612 | - "delete-devices-title": "您确定要删除{count,plural,1 {1 设备} other {# 设备}} 吗?", | |
613 | - "delete-devices-action-title": "删除 {count,plural,1 {1 设备} other {# 设备}}", | |
612 | + "delete-devices-title": "您确定要删除{count,plural,1 {1 设备} other {# 设备} } 吗?", | |
613 | + "delete-devices-action-title": "删除 {count,plural,1 {1 设备} other {# 设备} }", | |
614 | 614 | "delete-devices-text": "小心!确认后所有选定的设备将被删除,所有相关数据将不可恢复。", |
615 | 615 | "unassign-device-title": "您确定要取消分配设备 '{{deviceName}}'?", |
616 | 616 | "unassign-device-text": "确认后,设备将被取消分配,客户将无法访问。", |
617 | 617 | "unassign-device": "取消分配设备", |
618 | - "unassign-devices-title": "您确定要取消分配{count,plural,1 {1 设备} other {# 设备}} 吗?", | |
618 | + "unassign-devices-title": "您确定要取消分配{count,plural,1 {1 设备} other {# 设备} } 吗?", | |
619 | 619 | "unassign-devices-text": "确认后,所有选定的设备将被取消分配,并且客户将无法访问。", |
620 | 620 | "device-credentials": "设备凭据", |
621 | 621 | "credentials-type": "凭据类型", |
... | ... | @@ -792,7 +792,7 @@ |
792 | 792 | "delete-entity-views": "删除实体视图", |
793 | 793 | "unassign-from-customer": "取消分配客户", |
794 | 794 | "unassign-entity-views": "取消分配实体视图", |
795 | - "unassign-entity-views-action-title": "从客户处取消分配{count,plural,1 {1 实体视图} other {# 实体视图}}", | |
795 | + "unassign-entity-views-action-title": "从客户处取消分配{count,plural,1 {1 实体视图} other {# 实体视图} }", | |
796 | 796 | "assign-new-entity-view": "分配新实体视图", |
797 | 797 | "delete-entity-view-title": "确定要删除实体视图 '{{entityViewName}}'?", |
798 | 798 | "delete-entity-view-text": "小心!确认后实体视图及其所有相关数据将不可恢复。", |
... | ... | @@ -1192,7 +1192,7 @@ |
1192 | 1192 | "set-root-rulechain-text": "确认之后,规则链将变为根规格链,并将处理所有传入的传输消息。", |
1193 | 1193 | "delete-rulechain-title": " 确实要删除规则链'{{ruleChainName}}'吗?", |
1194 | 1194 | "delete-rulechain-text": "小心,在确认规则链和所有相关数据将变得不可恢复。", |
1195 | - "delete-rulechains-title": "确实要删除{count, plural, 1 { 1 规则链}其他{# 规则链库}}吗?", | |
1195 | + "delete-rulechains-title": "确实要删除{count, plural, 1 { 1 规则链} other {# 规则链库} }吗?", | |
1196 | 1196 | "delete-rulechains-action-title": "删除 { count, plural, 1 {1 规则链} other {# 规则链库} }", |
1197 | 1197 | "delete-rulechains-text": "小心,确认后,所有选定的规则链将被删除,所有相关的数据将变得不可恢复。", |
1198 | 1198 | "add-rulechain-text": "添加新的规则链", |
... | ... | @@ -1283,7 +1283,7 @@ |
1283 | 1283 | "tenant-details": "租客详情", |
1284 | 1284 | "delete-tenant-title": "您确定要删除租户'{{tenantTitle}}'吗?", |
1285 | 1285 | "delete-tenant-text": "小心!确认后,租户和所有相关数据将不可恢复。", |
1286 | - "delete-tenants-title": "您确定要删除 {count,plural,1 {1 租户} other {# 租户}} 吗?", | |
1286 | + "delete-tenants-title": "您确定要删除 {count,plural,1 {1 租户} other {# 租户} } 吗?", | |
1287 | 1287 | "delete-tenants-action-title": "删除 { count, plural, 1 {1 租户} other {# 租户} }", |
1288 | 1288 | "delete-tenants-text": "小心!确认后,所有选定的租户将被删除,所有相关数据将不可恢复。", |
1289 | 1289 | "title": "标题", | ... | ... |