93 changed files with 6717 additions and 145 deletions
@ -0,0 +1,55 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2019 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { Injectable } from '@angular/core'; |
||||
|
import { defaultHttpOptions } from './http-utils'; |
||||
|
import { Observable } from 'rxjs/index'; |
||||
|
import { HttpClient } from '@angular/common/http'; |
||||
|
import {AdminSettings, MailServerSettings, SecuritySettings} from '@shared/models/settings.models'; |
||||
|
|
||||
|
@Injectable({ |
||||
|
providedIn: 'root' |
||||
|
}) |
||||
|
export class AdminService { |
||||
|
|
||||
|
constructor( |
||||
|
private http: HttpClient |
||||
|
) { } |
||||
|
|
||||
|
public getAdminSettings<T>(key: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<AdminSettings<T>> { |
||||
|
return this.http.get<AdminSettings<T>>(`/api/admin/settings/${key}`, defaultHttpOptions(ignoreLoading, ignoreErrors)); |
||||
|
} |
||||
|
|
||||
|
public saveAdminSettings<T>(adminSettings: AdminSettings<T>, |
||||
|
ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<AdminSettings<T>> { |
||||
|
return this.http.post<AdminSettings<T>>('/api/admin/settings', adminSettings, defaultHttpOptions(ignoreLoading, ignoreErrors)); |
||||
|
} |
||||
|
|
||||
|
public sendTestMail(adminSettings: AdminSettings<MailServerSettings>, |
||||
|
ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<void> { |
||||
|
return this.http.post<void>('/api/admin/settings/testMail', adminSettings, defaultHttpOptions(ignoreLoading, ignoreErrors)); |
||||
|
} |
||||
|
|
||||
|
public getSecuritySettings(ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<SecuritySettings> { |
||||
|
return this.http.get<SecuritySettings>(`/api/admin/securitySettings`, defaultHttpOptions(ignoreLoading, ignoreErrors)); |
||||
|
} |
||||
|
|
||||
|
public saveSecuritySettings(securitySettings: SecuritySettings, |
||||
|
ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<SecuritySettings> { |
||||
|
return this.http.post<SecuritySettings>('/api/admin/securitySettings', securitySettings, |
||||
|
defaultHttpOptions(ignoreLoading, ignoreErrors)); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,52 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2019 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { Injectable } from '@angular/core'; |
||||
|
import { defaultHttpOptions } from './http-utils'; |
||||
|
import { Observable } from 'rxjs/index'; |
||||
|
import { HttpClient } from '@angular/common/http'; |
||||
|
import { PageLink } from '@shared/models/page/page-link'; |
||||
|
import { PageData } from '@shared/models/page/page-data'; |
||||
|
import { Customer } from '@shared/models/customer.model'; |
||||
|
|
||||
|
@Injectable({ |
||||
|
providedIn: 'root' |
||||
|
}) |
||||
|
export class CustomerService { |
||||
|
|
||||
|
constructor( |
||||
|
private http: HttpClient |
||||
|
) { } |
||||
|
|
||||
|
public getCustomers(tenantId: string, pageLink: PageLink, ignoreErrors: boolean = false, |
||||
|
ignoreLoading: boolean = false): Observable<PageData<Customer>> { |
||||
|
return this.http.get<PageData<Customer>>(`/api/tenant/${tenantId}/customers${pageLink.toQuery()}`, |
||||
|
defaultHttpOptions(ignoreLoading, ignoreErrors)); |
||||
|
} |
||||
|
|
||||
|
public getCustomer(customerId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Customer> { |
||||
|
return this.http.get<Customer>(`/api/customer/${customerId}`, defaultHttpOptions(ignoreLoading, ignoreErrors)); |
||||
|
} |
||||
|
|
||||
|
public saveCustomer(customer: Customer, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Customer> { |
||||
|
return this.http.post<Customer>('/api/customer', customer, defaultHttpOptions(ignoreLoading, ignoreErrors)); |
||||
|
} |
||||
|
|
||||
|
public deleteCustomer(customerId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false) { |
||||
|
return this.http.delete(`/api/customer/${customerId}`, defaultHttpOptions(ignoreLoading, ignoreErrors)); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,77 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2019 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { Injectable } from '@angular/core'; |
||||
|
import { defaultHttpOptions } from './http-utils'; |
||||
|
import { Observable } from 'rxjs/index'; |
||||
|
import { HttpClient } from '@angular/common/http'; |
||||
|
import { PageLink } from '@shared/models/page/page-link'; |
||||
|
import { PageData } from '@shared/models/page/page-data'; |
||||
|
import { Tenant } from '@shared/models/tenant.model'; |
||||
|
import {DashboardInfo, Dashboard} from '@shared/models/dashboard.models'; |
||||
|
import {map} from 'rxjs/operators'; |
||||
|
|
||||
|
@Injectable({ |
||||
|
providedIn: 'root' |
||||
|
}) |
||||
|
export class DashboardService { |
||||
|
|
||||
|
constructor( |
||||
|
private http: HttpClient |
||||
|
) { } |
||||
|
|
||||
|
public getTenantDashboards(pageLink: PageLink, ignoreErrors: boolean = false, |
||||
|
ignoreLoading: boolean = false): Observable<PageData<DashboardInfo>> { |
||||
|
return this.http.get<PageData<DashboardInfo>>(`/api/tenant/dashboards${pageLink.toQuery()}`, |
||||
|
defaultHttpOptions(ignoreLoading, ignoreErrors)); |
||||
|
} |
||||
|
|
||||
|
public getTenantDashboardsByTenantId(tenantId: string, pageLink: PageLink, ignoreErrors: boolean = false, |
||||
|
ignoreLoading: boolean = false): Observable<PageData<DashboardInfo>> { |
||||
|
return this.http.get<PageData<DashboardInfo>>(`/api/tenant/${tenantId}/dashboards${pageLink.toQuery()}`, |
||||
|
defaultHttpOptions(ignoreLoading, ignoreErrors)); |
||||
|
} |
||||
|
|
||||
|
public getCustomerDashboards(customerId: string, pageLink: PageLink, ignoreErrors: boolean = false, |
||||
|
ignoreLoading: boolean = false): Observable<PageData<DashboardInfo>> { |
||||
|
return this.http.get<PageData<DashboardInfo>>(`/api/customer/${customerId}/dashboards${pageLink.toQuery()}`, |
||||
|
defaultHttpOptions(ignoreLoading, ignoreErrors)).pipe( |
||||
|
map( dashboards => { |
||||
|
dashboards.data = dashboards.data.filter(dashboard => { |
||||
|
return dashboard.title.toUpperCase().includes(pageLink.textSearch.toUpperCase()); |
||||
|
}); |
||||
|
return dashboards; |
||||
|
} |
||||
|
)); |
||||
|
} |
||||
|
|
||||
|
public getDashboard(dashboardId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Dashboard> { |
||||
|
return this.http.get<Dashboard>(`/api/dashboard/${dashboardId}`, defaultHttpOptions(ignoreLoading, ignoreErrors)); |
||||
|
} |
||||
|
|
||||
|
public getDashboardInfo(dashboardId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<DashboardInfo> { |
||||
|
return this.http.get<DashboardInfo>(`/api/dashboard/info/${dashboardId}`, defaultHttpOptions(ignoreLoading, ignoreErrors)); |
||||
|
} |
||||
|
|
||||
|
public saveDashboard(dashboard: Dashboard, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Dashboard> { |
||||
|
return this.http.post<Dashboard>('/api/dashboard', dashboard, defaultHttpOptions(ignoreLoading, ignoreErrors)); |
||||
|
} |
||||
|
|
||||
|
public deleteDashboard(dashboardId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false) { |
||||
|
return this.http.delete(`/api/dashboard/${dashboardId}`, defaultHttpOptions(ignoreLoading, ignoreErrors)); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,50 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2019 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { Injectable } from '@angular/core'; |
||||
|
import { defaultHttpOptions } from './http-utils'; |
||||
|
import { Observable } from 'rxjs/index'; |
||||
|
import { HttpClient } from '@angular/common/http'; |
||||
|
import { PageLink } from '@shared/models/page/page-link'; |
||||
|
import { PageData } from '@shared/models/page/page-data'; |
||||
|
import { Tenant } from '@shared/models/tenant.model'; |
||||
|
|
||||
|
@Injectable({ |
||||
|
providedIn: 'root' |
||||
|
}) |
||||
|
export class TenantService { |
||||
|
|
||||
|
constructor( |
||||
|
private http: HttpClient |
||||
|
) { } |
||||
|
|
||||
|
public getTenants(pageLink: PageLink, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<PageData<Tenant>> { |
||||
|
return this.http.get<PageData<Tenant>>(`/api/tenants${pageLink.toQuery()}`, defaultHttpOptions(ignoreLoading, ignoreErrors)); |
||||
|
} |
||||
|
|
||||
|
public getTenant(tenantId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Tenant> { |
||||
|
return this.http.get<Tenant>(`/api/tenant/${tenantId}`, defaultHttpOptions(ignoreLoading, ignoreErrors)); |
||||
|
} |
||||
|
|
||||
|
public saveTenant(tenant: Tenant, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Tenant> { |
||||
|
return this.http.post<Tenant>('/api/tenant', tenant, defaultHttpOptions(ignoreLoading, ignoreErrors)); |
||||
|
} |
||||
|
|
||||
|
public deleteTenant(tenantId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false) { |
||||
|
return this.http.delete(`/api/tenant/${tenantId}`, defaultHttpOptions(ignoreLoading, ignoreErrors)); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,89 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2019 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { NgModule } from '@angular/core'; |
||||
|
import { Routes, RouterModule } from '@angular/router'; |
||||
|
|
||||
|
import { MailServerComponent } from '@modules/home/pages/admin/mail-server.component'; |
||||
|
import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard'; |
||||
|
import { Authority } from '@shared/models/authority.enum'; |
||||
|
import {GeneralSettingsComponent} from "@modules/home/pages/admin/general-settings.component"; |
||||
|
import {SecuritySettingsComponent} from "@modules/home/pages/admin/security-settings.component"; |
||||
|
|
||||
|
const routes: Routes = [ |
||||
|
{ |
||||
|
path: 'settings', |
||||
|
data: { |
||||
|
auth: [Authority.SYS_ADMIN], |
||||
|
breadcrumb: { |
||||
|
label: 'admin.system-settings', |
||||
|
icon: 'settings' |
||||
|
} |
||||
|
}, |
||||
|
children: [ |
||||
|
{ |
||||
|
path: '', |
||||
|
redirectTo: 'general', |
||||
|
pathMatch: 'full' |
||||
|
}, |
||||
|
{ |
||||
|
path: 'general', |
||||
|
component: GeneralSettingsComponent, |
||||
|
canDeactivate: [ConfirmOnExitGuard], |
||||
|
data: { |
||||
|
auth: [Authority.SYS_ADMIN], |
||||
|
title: 'admin.general-settings', |
||||
|
breadcrumb: { |
||||
|
label: 'admin.general', |
||||
|
icon: 'settings_applications' |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
path: 'outgoing-mail', |
||||
|
component: MailServerComponent, |
||||
|
canDeactivate: [ConfirmOnExitGuard], |
||||
|
data: { |
||||
|
auth: [Authority.SYS_ADMIN], |
||||
|
title: 'admin.outgoing-mail-settings', |
||||
|
breadcrumb: { |
||||
|
label: 'admin.outgoing-mail', |
||||
|
icon: 'mail' |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
path: 'security-settings', |
||||
|
component: SecuritySettingsComponent, |
||||
|
canDeactivate: [ConfirmOnExitGuard], |
||||
|
data: { |
||||
|
auth: [Authority.SYS_ADMIN], |
||||
|
title: 'admin.security-settings', |
||||
|
breadcrumb: { |
||||
|
label: 'admin.security-settings', |
||||
|
icon: 'security' |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
]; |
||||
|
|
||||
|
@NgModule({ |
||||
|
imports: [RouterModule.forChild(routes)], |
||||
|
exports: [RouterModule] |
||||
|
}) |
||||
|
export class AdminRoutingModule { } |
||||
@ -0,0 +1,39 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2019 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { NgModule } from '@angular/core'; |
||||
|
import { CommonModule } from '@angular/common'; |
||||
|
|
||||
|
import { AdminRoutingModule } from './admin-routing.module'; |
||||
|
import { SharedModule } from '@app/shared/shared.module'; |
||||
|
import { MailServerComponent } from '@modules/home/pages/admin/mail-server.component'; |
||||
|
import {GeneralSettingsComponent} from "@modules/home/pages/admin/general-settings.component"; |
||||
|
import {SecuritySettingsComponent} from "@modules/home/pages/admin/security-settings.component"; |
||||
|
|
||||
|
@NgModule({ |
||||
|
declarations: |
||||
|
[ |
||||
|
GeneralSettingsComponent, |
||||
|
MailServerComponent, |
||||
|
SecuritySettingsComponent |
||||
|
], |
||||
|
imports: [ |
||||
|
CommonModule, |
||||
|
SharedModule, |
||||
|
AdminRoutingModule |
||||
|
] |
||||
|
}) |
||||
|
export class AdminModule { } |
||||
@ -0,0 +1,47 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2019 The Thingsboard Authors |
||||
|
|
||||
|
Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
you may not use this file except in compliance with the License. |
||||
|
You may obtain a copy of the License at |
||||
|
|
||||
|
http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
|
||||
|
Unless required by applicable law or agreed to in writing, software |
||||
|
distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
See the License for the specific language governing permissions and |
||||
|
limitations under the License. |
||||
|
|
||||
|
--> |
||||
|
<div> |
||||
|
<mat-card class="settings-card"> |
||||
|
<mat-card-title> |
||||
|
<div fxLayout="row"> |
||||
|
<span class="mat-headline" translate>admin.general-settings</span> |
||||
|
</div> |
||||
|
</mat-card-title> |
||||
|
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> |
||||
|
</mat-progress-bar> |
||||
|
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div> |
||||
|
<mat-card-content style="padding-top: 16px;"> |
||||
|
<form #generalSettingsForm="ngForm" [formGroup]="generalSettings" (ngSubmit)="save()"> |
||||
|
<fieldset [disabled]="isLoading$ | async"> |
||||
|
<mat-form-field class="mat-block"> |
||||
|
<mat-label translate>admin.base-url</mat-label> |
||||
|
<input matInput formControlName="baseUrl" required/> |
||||
|
<mat-error *ngIf="generalSettings.get('baseUrl').hasError('required')"> |
||||
|
{{ 'admin.base-url-required' | translate }} |
||||
|
</mat-error> |
||||
|
</mat-form-field> |
||||
|
<div fxLayout="row" fxLayoutAlign="end center" style="width: 100%;" class="layout-wrap"> |
||||
|
<button mat-button mat-raised-button color="primary" [disabled]="(isLoading$ | async) || generalSettingsForm.invalid || !generalSettingsForm.dirty" |
||||
|
type="submit">{{'action.save' | translate}} |
||||
|
</button> |
||||
|
</div> |
||||
|
</fieldset> |
||||
|
</form> |
||||
|
</mat-card-content> |
||||
|
</mat-card> |
||||
|
</div> |
||||
@ -0,0 +1,18 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2019 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
:host { |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,77 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2019 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { Component, OnInit } from '@angular/core'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { PageComponent } from '@shared/components/page.component'; |
||||
|
import { Router } from '@angular/router'; |
||||
|
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; |
||||
|
import {AdminSettings, GeneralSettings} from '@shared/models/settings.models'; |
||||
|
import { AdminService } from '@core/http/admin.service'; |
||||
|
import { ActionNotificationShow } from '@core/notification/notification.actions'; |
||||
|
import { TranslateService } from '@ngx-translate/core'; |
||||
|
import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-general-settings', |
||||
|
templateUrl: './general-settings.component.html', |
||||
|
styleUrls: ['./general-settings.component.scss', './settings-card.scss'] |
||||
|
}) |
||||
|
export class GeneralSettingsComponent extends PageComponent implements OnInit, HasConfirmForm { |
||||
|
|
||||
|
generalSettings: FormGroup; |
||||
|
adminSettings: AdminSettings<GeneralSettings>; |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
private router: Router, |
||||
|
private adminService: AdminService, |
||||
|
private translate: TranslateService, |
||||
|
public fb: FormBuilder) { |
||||
|
super(store); |
||||
|
} |
||||
|
|
||||
|
ngOnInit() { |
||||
|
this.buildGeneralServerSettingsForm(); |
||||
|
this.adminService.getAdminSettings<GeneralSettings>('general').subscribe( |
||||
|
(adminSettings) => { |
||||
|
this.adminSettings = adminSettings; |
||||
|
this.generalSettings.reset(this.adminSettings.jsonValue); |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
buildGeneralServerSettingsForm() { |
||||
|
this.generalSettings = this.fb.group({ |
||||
|
baseUrl: ['', [Validators.required]] |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
save(): void { |
||||
|
this.adminSettings.jsonValue = {...this.adminSettings.jsonValue, ...this.generalSettings.value}; |
||||
|
this.adminService.saveAdminSettings(this.adminSettings).subscribe( |
||||
|
(adminSettings) => { |
||||
|
this.adminSettings = adminSettings; |
||||
|
this.generalSettings.reset(this.adminSettings.jsonValue); |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
confirmForm(): FormGroup { |
||||
|
return this.generalSettings; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,104 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2019 The Thingsboard Authors |
||||
|
|
||||
|
Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
you may not use this file except in compliance with the License. |
||||
|
You may obtain a copy of the License at |
||||
|
|
||||
|
http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
|
||||
|
Unless required by applicable law or agreed to in writing, software |
||||
|
distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
See the License for the specific language governing permissions and |
||||
|
limitations under the License. |
||||
|
|
||||
|
--> |
||||
|
<div> |
||||
|
<mat-card class="settings-card"> |
||||
|
<mat-card-title> |
||||
|
<div fxLayout="row"> |
||||
|
<span class="mat-headline" translate>admin.outgoing-mail-settings</span> |
||||
|
<span fxFlex></span> |
||||
|
<div tb-help="outgoingMailSettings"></div> |
||||
|
</div> |
||||
|
</mat-card-title> |
||||
|
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> |
||||
|
</mat-progress-bar> |
||||
|
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div> |
||||
|
<mat-card-content style="padding-top: 16px;"> |
||||
|
<form #mailSettingsForm="ngForm" [formGroup]="mailSettings" (ngSubmit)="save()"> |
||||
|
<fieldset [disabled]="isLoading$ | async"> |
||||
|
<mat-form-field class="mat-block"> |
||||
|
<mat-label translate>admin.mail-from</mat-label> |
||||
|
<input matInput formControlName="mailFrom" required/> |
||||
|
<mat-error *ngIf="mailSettings.get('mailFrom').hasError('required')"> |
||||
|
{{ 'admin.mail-from-required' | translate }} |
||||
|
</mat-error> |
||||
|
</mat-form-field> |
||||
|
<mat-form-field class="mat-block"> |
||||
|
<mat-label translate>admin.smtp-protocol</mat-label> |
||||
|
<mat-select matInput formControlName="smtpProtocol"> |
||||
|
<mat-option *ngFor="let protocol of smtpProtocols" [value]="protocol"> |
||||
|
{{protocol.toUpperCase()}} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
<div fxLayout.gt-sm="row" fxLayoutGap.gt-sm="10px"> |
||||
|
<mat-form-field class="mat-block" fxFlex="100" fxFlex.gt-sm="60"> |
||||
|
<mat-label translate>admin.smtp-host</mat-label> |
||||
|
<input matInput formControlName="smtpHost" placeholder="localhost" required/> |
||||
|
<mat-error *ngIf="mailSettings.get('smtpHost').hasError('required')"> |
||||
|
{{ 'admin.smtp-host-required' | translate }} |
||||
|
</mat-error> |
||||
|
</mat-form-field> |
||||
|
<mat-form-field class="mat-block" fxFlex="100" fxFlex.gt-sm="40"> |
||||
|
<mat-label translate>admin.smtp-port</mat-label> |
||||
|
<input matInput #smtpPortInput formControlName="smtpPort" placeholder="25" maxlength="5" required/> |
||||
|
<mat-hint align="end">{{smtpPortInput.value?.length || 0}}/5</mat-hint> |
||||
|
<mat-error *ngIf="mailSettings.get('smtpPort').hasError('required')"> |
||||
|
{{ 'admin.smtp-port-required' | translate }} |
||||
|
</mat-error> |
||||
|
<mat-error *ngIf="mailSettings.get('smtpPort').hasError('pattern') || mailSettings.get('smtpPort').hasError('maxlength')"> |
||||
|
{{ 'admin.smtp-port-invalid' | translate }} |
||||
|
</mat-error> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
<mat-form-field class="mat-block"> |
||||
|
<mat-label translate>admin.timeout-msec</mat-label> |
||||
|
<input matInput #timeoutInput formControlName="timeout" placeholder="10000" maxlength="6" required/> |
||||
|
<mat-hint align="end">{{timeoutInput.value?.length || 0}}/6</mat-hint> |
||||
|
<mat-error *ngIf="mailSettings.get('timeout').hasError('required')"> |
||||
|
{{ 'admin.timeout-required' | translate }} |
||||
|
</mat-error> |
||||
|
<mat-error *ngIf="mailSettings.get('timeout').hasError('pattern') || mailSettings.get('timeout').hasError('maxlength')"> |
||||
|
{{ 'admin.timeout-invalid' | translate }} |
||||
|
</mat-error> |
||||
|
</mat-form-field> |
||||
|
<tb-checkbox formControlName="enableTls" trueValue="true" falseValue="false"> |
||||
|
{{ 'admin.enable-tls' | translate }} |
||||
|
</tb-checkbox> |
||||
|
<mat-form-field class="mat-block"> |
||||
|
<mat-label translate>common.username</mat-label> |
||||
|
<input matInput formControlName="username" placeholder="{{ 'common.enter-username' | translate }}"/> |
||||
|
</mat-form-field> |
||||
|
<mat-form-field class="mat-block"> |
||||
|
<mat-label translate>common.password</mat-label> |
||||
|
<input matInput formControlName="password" type="password" placeholder="{{ 'common.enter-password' | translate }}"/> |
||||
|
</mat-form-field> |
||||
|
<div fxLayout="row" fxLayoutAlign="end center" style="width: 100%;" class="layout-wrap"> |
||||
|
<button mat-button mat-raised-button |
||||
|
type="button" style="margin-right: 16px;" |
||||
|
[disabled]="(isLoading$ | async) || mailSettingsForm.invalid" (click)="sendTestMail()"> |
||||
|
{{'admin.send-test-mail' | translate}} |
||||
|
</button> |
||||
|
<button mat-button mat-raised-button color="primary" [disabled]="(isLoading$ | async) || mailSettingsForm.invalid || !mailSettingsForm.dirty" |
||||
|
type="submit">{{'action.save' | translate}} |
||||
|
</button> |
||||
|
</div> |
||||
|
</fieldset> |
||||
|
</form> |
||||
|
</mat-card-content> |
||||
|
</mat-card> |
||||
|
</div> |
||||
@ -0,0 +1,18 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2019 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
:host { |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,101 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2019 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { Component, OnInit } from '@angular/core'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { PageComponent } from '@shared/components/page.component'; |
||||
|
import { Router } from '@angular/router'; |
||||
|
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; |
||||
|
import { AdminSettings, MailServerSettings, smtpPortPattern } from '@shared/models/settings.models'; |
||||
|
import { AdminService } from '@core/http/admin.service'; |
||||
|
import { ActionNotificationShow } from '@core/notification/notification.actions'; |
||||
|
import { TranslateService } from '@ngx-translate/core'; |
||||
|
import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-mail-server', |
||||
|
templateUrl: './mail-server.component.html', |
||||
|
styleUrls: ['./mail-server.component.scss', './settings-card.scss'] |
||||
|
}) |
||||
|
export class MailServerComponent extends PageComponent implements OnInit, HasConfirmForm { |
||||
|
|
||||
|
mailSettings: FormGroup; |
||||
|
adminSettings: AdminSettings<MailServerSettings>; |
||||
|
smtpProtocols = ['smtp', 'smtps']; |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
private router: Router, |
||||
|
private adminService: AdminService, |
||||
|
private translate: TranslateService, |
||||
|
public fb: FormBuilder) { |
||||
|
super(store); |
||||
|
} |
||||
|
|
||||
|
ngOnInit() { |
||||
|
this.buildMailServerSettingsForm(); |
||||
|
this.adminService.getAdminSettings<MailServerSettings>('mail').subscribe( |
||||
|
(adminSettings) => { |
||||
|
this.adminSettings = adminSettings; |
||||
|
this.mailSettings.reset(this.adminSettings.jsonValue); |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
buildMailServerSettingsForm() { |
||||
|
this.mailSettings = this.fb.group({ |
||||
|
mailFrom: ['', [Validators.required]], |
||||
|
smtpProtocol: ['smtp'], |
||||
|
smtpHost: ['localhost', [Validators.required]], |
||||
|
smtpPort: ['25', [Validators.required, |
||||
|
Validators.pattern(smtpPortPattern), |
||||
|
Validators.maxLength(5)]], |
||||
|
timeout: ['10000', [Validators.required, |
||||
|
Validators.pattern(/^[0-9]{1,6}$/), |
||||
|
Validators.maxLength(6)]], |
||||
|
enableTls: ['false'], |
||||
|
username: [''], |
||||
|
password: [''] |
||||
|
}); |
||||
|
this.registerDisableOnLoadFormControl(this.mailSettings.get('smtpProtocol')); |
||||
|
this.registerDisableOnLoadFormControl(this.mailSettings.get('enableTls')); |
||||
|
} |
||||
|
|
||||
|
sendTestMail(): void { |
||||
|
this.adminSettings.jsonValue = {...this.adminSettings.jsonValue, ...this.mailSettings.value}; |
||||
|
this.adminService.sendTestMail(this.adminSettings).subscribe( |
||||
|
() => { |
||||
|
this.store.dispatch(new ActionNotificationShow({ message: this.translate.instant('admin.test-mail-sent'), |
||||
|
type: 'success' })); |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
save(): void { |
||||
|
this.adminSettings.jsonValue = {...this.adminSettings.jsonValue, ...this.mailSettings.value}; |
||||
|
this.adminService.saveAdminSettings(this.adminSettings).subscribe( |
||||
|
(adminSettings) => { |
||||
|
this.adminSettings = adminSettings; |
||||
|
this.mailSettings.reset(this.adminSettings.jsonValue); |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
confirmForm(): FormGroup { |
||||
|
return this.mailSettings; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,119 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2019 The Thingsboard Authors |
||||
|
|
||||
|
Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
you may not use this file except in compliance with the License. |
||||
|
You may obtain a copy of the License at |
||||
|
|
||||
|
http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
|
||||
|
Unless required by applicable law or agreed to in writing, software |
||||
|
distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
See the License for the specific language governing permissions and |
||||
|
limitations under the License. |
||||
|
|
||||
|
--> |
||||
|
<div> |
||||
|
<mat-card class="settings-card"> |
||||
|
<mat-card-title> |
||||
|
<div fxLayout="row"> |
||||
|
<span class="mat-headline" translate>admin.security-settings</span> |
||||
|
<span fxFlex></span> |
||||
|
<div tb-help="securitySettings"></div> |
||||
|
</div> |
||||
|
</mat-card-title> |
||||
|
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> |
||||
|
</mat-progress-bar> |
||||
|
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div> |
||||
|
<mat-card-content style="padding-top: 16px;"> |
||||
|
<form #securitySettingsForm="ngForm" [formGroup]="securitySettingsFormGroup" (ngSubmit)="save()"> |
||||
|
<fieldset [disabled]="isLoading$ | async"> |
||||
|
<mat-expansion-panel [expanded]="true"> |
||||
|
<mat-expansion-panel-header> |
||||
|
<mat-panel-title> |
||||
|
<div class="tb-panel-title" translate>admin.password-policy</div> |
||||
|
</mat-panel-title> |
||||
|
</mat-expansion-panel-header> |
||||
|
<section formGroupName="passwordPolicy"> |
||||
|
<mat-form-field class="mat-block"> |
||||
|
<mat-label translate>admin.minimum-password-length</mat-label> |
||||
|
<input matInput type="number" |
||||
|
formControlName="minimumLength" |
||||
|
step="1" |
||||
|
min="5" |
||||
|
max="50" |
||||
|
required/> |
||||
|
<mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy').get('minimumLength').hasError('required')"> |
||||
|
{{ 'admin.minimum-password-length-required' | translate }} |
||||
|
</mat-error> |
||||
|
<mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy').get('minimumLength').hasError('min')"> |
||||
|
{{ 'admin.minimum-password-length-range' | translate }} |
||||
|
</mat-error> |
||||
|
<mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy').get('minimumLength').hasError('max')"> |
||||
|
{{ 'admin.minimum-password-length-range' | translate }} |
||||
|
</mat-error> |
||||
|
</mat-form-field> |
||||
|
<mat-form-field class="mat-block"> |
||||
|
<mat-label translate>admin.minimum-uppercase-letters</mat-label> |
||||
|
<input matInput type="number" |
||||
|
formControlName="minimumUppercaseLetters" |
||||
|
step="1" |
||||
|
min="0"/> |
||||
|
<mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy').get('minimumUppercaseLetters').hasError('min')"> |
||||
|
{{ 'admin.minimum-uppercase-letters-range' | translate }} |
||||
|
</mat-error> |
||||
|
</mat-form-field> |
||||
|
<mat-form-field class="mat-block"> |
||||
|
<mat-label translate>admin.minimum-lowercase-letters</mat-label> |
||||
|
<input matInput type="number" |
||||
|
formControlName="minimumLowercaseLetters" |
||||
|
step="1" |
||||
|
min="0"/> |
||||
|
<mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy').get('minimumLowercaseLetters').hasError('min')"> |
||||
|
{{ 'admin.minimum-lowercase-letters-range' | translate }} |
||||
|
</mat-error> |
||||
|
</mat-form-field> |
||||
|
<mat-form-field class="mat-block"> |
||||
|
<mat-label translate>admin.minimum-digits</mat-label> |
||||
|
<input matInput type="number" |
||||
|
formControlName="minimumDigits" |
||||
|
step="1" |
||||
|
min="0"/> |
||||
|
<mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy').get('minimumDigits').hasError('min')"> |
||||
|
{{ 'admin.minimum-digits-range' | translate }} |
||||
|
</mat-error> |
||||
|
</mat-form-field> |
||||
|
<mat-form-field class="mat-block"> |
||||
|
<mat-label translate>admin.minimum-special-characters</mat-label> |
||||
|
<input matInput type="number" |
||||
|
formControlName="minimumSpecialCharacters" |
||||
|
step="1" |
||||
|
min="0"/> |
||||
|
<mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy').get('minimumSpecialCharacters').hasError('min')"> |
||||
|
{{ 'admin.minimum-special-characters-range' | translate }} |
||||
|
</mat-error> |
||||
|
</mat-form-field> |
||||
|
<mat-form-field class="mat-block"> |
||||
|
<mat-label translate>admin.password-expiration-period-days</mat-label> |
||||
|
<input matInput type="number" |
||||
|
formControlName="passwordExpirationPeriodDays" |
||||
|
step="1" |
||||
|
min="0"/> |
||||
|
<mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy').get('passwordExpirationPeriodDays').hasError('min')"> |
||||
|
{{ 'admin.password-expiration-period-days-range' | translate }} |
||||
|
</mat-error> |
||||
|
</mat-form-field> |
||||
|
</section> |
||||
|
</mat-expansion-panel> |
||||
|
<div fxLayout="row" fxLayoutAlign="end center" style="width: 100%;" class="layout-wrap"> |
||||
|
<button mat-button mat-raised-button color="primary" [disabled]="(isLoading$ | async) || securitySettingsForm.invalid || !securitySettingsForm.dirty" |
||||
|
type="submit">{{'action.save' | translate}} |
||||
|
</button> |
||||
|
</div> |
||||
|
</fieldset> |
||||
|
</form> |
||||
|
</mat-card-content> |
||||
|
</mat-card> |
||||
|
</div> |
||||
@ -0,0 +1,23 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2019 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
:host { |
||||
|
mat-expansion-panel { |
||||
|
margin-bottom: 16px; |
||||
|
} |
||||
|
.tb-panel-title { |
||||
|
|
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,86 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2019 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { Component, OnInit } from '@angular/core'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { PageComponent } from '@shared/components/page.component'; |
||||
|
import { Router } from '@angular/router'; |
||||
|
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; |
||||
|
import { SecuritySettings} from '@shared/models/settings.models'; |
||||
|
import { AdminService } from '@core/http/admin.service'; |
||||
|
import { ActionNotificationShow } from '@core/notification/notification.actions'; |
||||
|
import { TranslateService } from '@ngx-translate/core'; |
||||
|
import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-security-settings', |
||||
|
templateUrl: './security-settings.component.html', |
||||
|
styleUrls: ['./security-settings.component.scss', './settings-card.scss'] |
||||
|
}) |
||||
|
export class SecuritySettingsComponent extends PageComponent implements OnInit, HasConfirmForm { |
||||
|
|
||||
|
securitySettingsFormGroup: FormGroup; |
||||
|
securitySettings: SecuritySettings; |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
private router: Router, |
||||
|
private adminService: AdminService, |
||||
|
private translate: TranslateService, |
||||
|
public fb: FormBuilder) { |
||||
|
super(store); |
||||
|
} |
||||
|
|
||||
|
ngOnInit() { |
||||
|
this.buildSecuritySettingsForm(); |
||||
|
this.adminService.getSecuritySettings().subscribe( |
||||
|
(securitySettings) => { |
||||
|
this.securitySettings = securitySettings; |
||||
|
this.securitySettingsFormGroup.reset(this.securitySettings); |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
buildSecuritySettingsForm() { |
||||
|
this.securitySettingsFormGroup = this.fb.group({ |
||||
|
passwordPolicy: this.fb.group( |
||||
|
{ |
||||
|
minimumLength: [null, [Validators.required, Validators.min(5), Validators.max(50)]], |
||||
|
minimumUppercaseLetters: [null, Validators.min(0)], |
||||
|
minimumLowercaseLetters: [null, Validators.min(0)], |
||||
|
minimumDigits: [null, Validators.min(0)], |
||||
|
minimumSpecialCharacters: [null, Validators.min(0)], |
||||
|
passwordExpirationPeriodDays: [null, Validators.min(0)] |
||||
|
} |
||||
|
) |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
save(): void { |
||||
|
this.securitySettings = {...this.securitySettings, ...this.securitySettingsFormGroup.value}; |
||||
|
this.adminService.saveSecuritySettings(this.securitySettings).subscribe( |
||||
|
(securitySettings) => { |
||||
|
this.securitySettings = securitySettings; |
||||
|
this.securitySettingsFormGroup.reset(this.securitySettings); |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
confirmForm(): FormGroup { |
||||
|
return this.securitySettingsFormGroup; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,25 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2019 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
@import "../../../../../scss/constants"; |
||||
|
|
||||
|
:host { |
||||
|
mat-card.settings-card { |
||||
|
margin: 8px; |
||||
|
@media #{$mat-gt-sm} { |
||||
|
width: 60%; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,63 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2019 The Thingsboard Authors |
||||
|
|
||||
|
Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
you may not use this file except in compliance with the License. |
||||
|
You may obtain a copy of the License at |
||||
|
|
||||
|
http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
|
||||
|
Unless required by applicable law or agreed to in writing, software |
||||
|
distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
See the License for the specific language governing permissions and |
||||
|
limitations under the License. |
||||
|
|
||||
|
--> |
||||
|
<form #changePasswordForm="ngForm" [formGroup]="changePassword" (ngSubmit)="onChangePassword()"> |
||||
|
<mat-toolbar fxLayout="row" color="primary"> |
||||
|
<h2 translate>profile.change-password</h2> |
||||
|
<span fxFlex></span> |
||||
|
<button mat-button mat-icon-button |
||||
|
[mat-dialog-close]="false" |
||||
|
type="button"> |
||||
|
<mat-icon class="material-icons">close</mat-icon> |
||||
|
</button> |
||||
|
</mat-toolbar> |
||||
|
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> |
||||
|
</mat-progress-bar> |
||||
|
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div> |
||||
|
<div mat-dialog-content> |
||||
|
<mat-form-field class="mat-block"> |
||||
|
<mat-label translate>profile.current-password</mat-label> |
||||
|
<input matInput type="password" formControlName="currentPassword"/> |
||||
|
<mat-icon class="material-icons" matPrefix>lock</mat-icon> |
||||
|
</mat-form-field> |
||||
|
<mat-form-field class="mat-block"> |
||||
|
<mat-label translate>login.new-password</mat-label> |
||||
|
<input matInput type="password" formControlName="newPassword"/> |
||||
|
<mat-icon class="material-icons" matPrefix>lock</mat-icon> |
||||
|
</mat-form-field> |
||||
|
<mat-form-field class="mat-block"> |
||||
|
<mat-label translate>login.new-password-again</mat-label> |
||||
|
<input matInput type="password" formControlName="newPassword2"/> |
||||
|
<mat-icon class="material-icons" matPrefix>lock</mat-icon> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
<div mat-dialog-actions fxLayout="row"> |
||||
|
<span fxFlex></span> |
||||
|
<button mat-button mat-raised-button color="primary" |
||||
|
type="submit" |
||||
|
[disabled]="(isLoading$ | async) || changePasswordForm.invalid"> |
||||
|
{{ 'profile.change-password' | translate }} |
||||
|
</button> |
||||
|
<button mat-button color="primary" |
||||
|
style="margin-right: 20px;" |
||||
|
type="button" |
||||
|
[disabled]="(isLoading$ | async)" |
||||
|
[mat-dialog-close]="false" cdkFocusInitial> |
||||
|
{{ 'action.cancel' | translate }} |
||||
|
</button> |
||||
|
</div> |
||||
|
</form> |
||||
@ -0,0 +1,17 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2019 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
:host { |
||||
|
} |
||||
@ -0,0 +1,68 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2019 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { Component, OnInit } from '@angular/core'; |
||||
|
import { MatDialogRef } from '@angular/material'; |
||||
|
import { PageComponent } from '@shared/components/page.component'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; |
||||
|
import { ActionNotificationShow } from '@core/notification/notification.actions'; |
||||
|
import { TranslateService } from '@ngx-translate/core'; |
||||
|
import { AuthService } from '@core/auth/auth.service'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-change-password-dialog', |
||||
|
templateUrl: './change-password-dialog.component.html', |
||||
|
styleUrls: ['./change-password-dialog.component.scss'] |
||||
|
}) |
||||
|
export class ChangePasswordDialogComponent extends PageComponent implements OnInit { |
||||
|
|
||||
|
changePassword: FormGroup; |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
private translate: TranslateService, |
||||
|
private authService: AuthService, |
||||
|
public dialogRef: MatDialogRef<ChangePasswordDialogComponent>, |
||||
|
public fb: FormBuilder) { |
||||
|
super(store); |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
this.buildChangePasswordForm(); |
||||
|
} |
||||
|
|
||||
|
buildChangePasswordForm() { |
||||
|
this.changePassword = this.fb.group({ |
||||
|
currentPassword: [''], |
||||
|
newPassword: [''], |
||||
|
newPassword2: [''] |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
onChangePassword(): void { |
||||
|
if (this.changePassword.get('newPassword').value !== this.changePassword.get('newPassword2').value) { |
||||
|
this.store.dispatch(new ActionNotificationShow({ message: this.translate.instant('login.passwords-mismatch-error'), |
||||
|
type: 'error' })); |
||||
|
} else { |
||||
|
this.authService.changePassword( |
||||
|
this.changePassword.get('currentPassword').value, |
||||
|
this.changePassword.get('newPassword').value).subscribe(() => { |
||||
|
this.dialogRef.close(true); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,69 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2019 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import {Injectable, NgModule} from '@angular/core'; |
||||
|
import {Resolve, RouterModule, Routes} from '@angular/router'; |
||||
|
|
||||
|
import {ProfileComponent} from './profile.component'; |
||||
|
import {ConfirmOnExitGuard} from '@core/guards/confirm-on-exit.guard'; |
||||
|
import {Authority} from '@shared/models/authority.enum'; |
||||
|
import {User} from '@shared/models/user.model'; |
||||
|
import {Store} from '@ngrx/store'; |
||||
|
import {AppState} from '@core/core.state'; |
||||
|
import {UserService} from '@core/http/user.service'; |
||||
|
import {getCurrentAuthUser} from '@core/auth/auth.selectors'; |
||||
|
import {Observable} from 'rxjs'; |
||||
|
|
||||
|
@Injectable() |
||||
|
export class UserProfileResolver implements Resolve<User> { |
||||
|
|
||||
|
constructor(private store: Store<AppState>, |
||||
|
private userService: UserService) { |
||||
|
} |
||||
|
|
||||
|
resolve(): Observable<User> { |
||||
|
const userId = getCurrentAuthUser(this.store).userId; |
||||
|
return this.userService.getUser(userId); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const routes: Routes = [ |
||||
|
{ |
||||
|
path: 'profile', |
||||
|
component: ProfileComponent, |
||||
|
canDeactivate: [ConfirmOnExitGuard], |
||||
|
data: { |
||||
|
auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN, Authority.CUSTOMER_USER], |
||||
|
title: 'profile.profile', |
||||
|
breadcrumb: { |
||||
|
label: 'profile.profile', |
||||
|
icon: 'account_circle' |
||||
|
} |
||||
|
}, |
||||
|
resolve: { |
||||
|
user: UserProfileResolver |
||||
|
} |
||||
|
} |
||||
|
]; |
||||
|
|
||||
|
@NgModule({ |
||||
|
imports: [RouterModule.forChild(routes)], |
||||
|
exports: [RouterModule], |
||||
|
providers: [ |
||||
|
UserProfileResolver |
||||
|
] |
||||
|
}) |
||||
|
export class ProfileRoutingModule { } |
||||
@ -0,0 +1,77 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2019 The Thingsboard Authors |
||||
|
|
||||
|
Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
you may not use this file except in compliance with the License. |
||||
|
You may obtain a copy of the License at |
||||
|
|
||||
|
http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
|
||||
|
Unless required by applicable law or agreed to in writing, software |
||||
|
distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
See the License for the specific language governing permissions and |
||||
|
limitations under the License. |
||||
|
|
||||
|
--> |
||||
|
<div> |
||||
|
<mat-card class="profile-card"> |
||||
|
<mat-card-title> |
||||
|
<div fxLayout="column"> |
||||
|
<span class="mat-headline" translate>profile.profile</span> |
||||
|
<span class="profile-email" style='opacity: 0.7;'>{{ profile ? profile.get('email').value : '' }}</span> |
||||
|
</div> |
||||
|
</mat-card-title> |
||||
|
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> |
||||
|
</mat-progress-bar> |
||||
|
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div> |
||||
|
<mat-card-content style="padding-top: 16px;"> |
||||
|
<form #profileForm="ngForm" [formGroup]="profile" (ngSubmit)="save()"> |
||||
|
<fieldset [disabled]="isLoading$ | async"> |
||||
|
<mat-form-field class="mat-block"> |
||||
|
<mat-label translate>user.email</mat-label> |
||||
|
<input matInput formControlName="email" required/> |
||||
|
<mat-error *ngIf="profile.get('email').hasError('required')"> |
||||
|
{{ 'user.email-required' | translate }} |
||||
|
</mat-error> |
||||
|
<mat-error *ngIf="profile.get('email').hasError('email')"> |
||||
|
{{ 'user.invalid-email-format' | translate }} |
||||
|
</mat-error> |
||||
|
</mat-form-field> |
||||
|
<mat-form-field class="mat-block"> |
||||
|
<mat-label translate>user.first-name</mat-label> |
||||
|
<input matInput formControlName="firstName"/> |
||||
|
</mat-form-field> |
||||
|
<mat-form-field class="mat-block"> |
||||
|
<mat-label translate>user.last-name</mat-label> |
||||
|
<input matInput formControlName="lastName"/> |
||||
|
</mat-form-field> |
||||
|
<mat-form-field class="mat-block"> |
||||
|
<mat-label translate>language.language</mat-label> |
||||
|
<mat-select matInput formControlName="language"> |
||||
|
<mat-option *ngFor="let lang of languageList" [value]="lang"> |
||||
|
{{ lang ? ('language.locales.' + lang | translate) : ''}} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
<div fxLayout="row" style="padding-bottom: 16px;"> |
||||
|
<button mat-button mat-raised-button color="primary" |
||||
|
type="button" |
||||
|
[disabled]="(isLoading$ | async)" (click)="changePassword()"> |
||||
|
{{'profile.change-password' | translate}} |
||||
|
</button> |
||||
|
</div> |
||||
|
<div fxLayout="row" class="layout-wrap"> |
||||
|
<span fxFlex></span> |
||||
|
<button mat-button mat-raised-button color="primary" |
||||
|
type="submit" |
||||
|
[disabled]="(isLoading$ | async) || profileForm.invalid || !profileForm.dirty"> |
||||
|
{{ 'action.save' | translate }} |
||||
|
</button> |
||||
|
</div> |
||||
|
</fieldset> |
||||
|
</form> |
||||
|
</mat-card-content> |
||||
|
</mat-card> |
||||
|
</div> |
||||
@ -0,0 +1,32 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2019 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
@import "../../../../../scss/constants"; |
||||
|
|
||||
|
:host { |
||||
|
mat-card.profile-card { |
||||
|
margin: 8px; |
||||
|
@media #{$mat-gt-sm} { |
||||
|
width: 60%; |
||||
|
} |
||||
|
.mat-headline { |
||||
|
margin: 0; |
||||
|
} |
||||
|
.profile-email { |
||||
|
font-size: 16px; |
||||
|
font-weight: 400; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,123 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2019 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { Component, OnInit } from '@angular/core'; |
||||
|
import { UserService } from '@core/http/user.service'; |
||||
|
import { User } from '@shared/models/user.model'; |
||||
|
import { Authority } from '@shared/models/authority.enum'; |
||||
|
import { PageComponent } from '@shared/components/page.component'; |
||||
|
import { select, Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { getCurrentAuthUser, selectAuthUser } from '@core/auth/auth.selectors'; |
||||
|
import { mergeMap, take } from 'rxjs/operators'; |
||||
|
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; |
||||
|
import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard'; |
||||
|
import { ActionAuthUpdateUserDetails } from '@core/auth/auth.actions'; |
||||
|
import { environment as env } from '@env/environment'; |
||||
|
import { TranslateService } from '@ngx-translate/core'; |
||||
|
import { ActionSettingsChangeLanguage } from '@core/settings/settings.actions'; |
||||
|
import { ChangePasswordDialogComponent } from '@modules/home/pages/profile/change-password-dialog.component'; |
||||
|
import { MatDialog } from '@angular/material'; |
||||
|
import { DialogService } from '@core/services/dialog.service'; |
||||
|
import { AuthService } from '@core/auth/auth.service'; |
||||
|
import { ActivatedRoute } from '@angular/router'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-profile', |
||||
|
templateUrl: './profile.component.html', |
||||
|
styleUrls: ['./profile.component.scss'] |
||||
|
}) |
||||
|
export class ProfileComponent extends PageComponent implements OnInit, HasConfirmForm { |
||||
|
|
||||
|
authorities = Authority; |
||||
|
profile: FormGroup; |
||||
|
user: User; |
||||
|
languageList = env.supportedLangs; |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
private route: ActivatedRoute, |
||||
|
private userService: UserService, |
||||
|
private authService: AuthService, |
||||
|
private translate: TranslateService, |
||||
|
public dialog: MatDialog, |
||||
|
public dialogService: DialogService, |
||||
|
public fb: FormBuilder) { |
||||
|
super(store); |
||||
|
} |
||||
|
|
||||
|
ngOnInit() { |
||||
|
this.buildProfileForm(); |
||||
|
this.userLoaded(this.route.snapshot.data.user); |
||||
|
} |
||||
|
|
||||
|
buildProfileForm() { |
||||
|
this.profile = this.fb.group({ |
||||
|
email: ['', [Validators.required, Validators.email]], |
||||
|
firstName: [''], |
||||
|
lastName: [''], |
||||
|
language: [''] |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
save(): void { |
||||
|
this.user = {...this.user, ...this.profile.value}; |
||||
|
if (!this.user.additionalInfo) { |
||||
|
this.user.additionalInfo = {}; |
||||
|
} |
||||
|
this.user.additionalInfo.lang = this.profile.get('language').value; |
||||
|
this.userService.saveUser(this.user).subscribe( |
||||
|
(user) => { |
||||
|
this.userLoaded(user); |
||||
|
this.store.dispatch(new ActionAuthUpdateUserDetails({ userDetails: { |
||||
|
additionalInfo: {...user.additionalInfo}, |
||||
|
authority: user.authority, |
||||
|
createdTime: user.createdTime, |
||||
|
tenantId: user.tenantId, |
||||
|
customerId: user.customerId, |
||||
|
email: user.email, |
||||
|
firstName: user.firstName, |
||||
|
id: user.id, |
||||
|
lastName: user.lastName, |
||||
|
} })); |
||||
|
this.store.dispatch(new ActionSettingsChangeLanguage({ userLang: user.additionalInfo.lang })); |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
changePassword(): void { |
||||
|
this.dialog.open(ChangePasswordDialogComponent, { |
||||
|
disableClose: true, |
||||
|
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'] |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
userLoaded(user: User) { |
||||
|
this.user = user; |
||||
|
this.profile.reset(user); |
||||
|
let lang; |
||||
|
if (user.additionalInfo && user.additionalInfo.lang) { |
||||
|
lang = user.additionalInfo.lang; |
||||
|
} else { |
||||
|
lang = this.translate.currentLang; |
||||
|
} |
||||
|
this.profile.get('language').setValue(lang); |
||||
|
} |
||||
|
|
||||
|
confirmForm(): FormGroup { |
||||
|
return this.profile; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,38 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2019 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { NgModule } from '@angular/core'; |
||||
|
import { CommonModule } from '@angular/common'; |
||||
|
import { ProfileComponent } from './profile.component'; |
||||
|
import { SharedModule } from '@shared/shared.module'; |
||||
|
import { ProfileRoutingModule } from './profile-routing.module'; |
||||
|
import { ChangePasswordDialogComponent } from '@modules/home/pages/profile/change-password-dialog.component'; |
||||
|
|
||||
|
@NgModule({ |
||||
|
entryComponents: [ |
||||
|
ChangePasswordDialogComponent |
||||
|
], |
||||
|
declarations: [ |
||||
|
ProfileComponent, |
||||
|
ChangePasswordDialogComponent |
||||
|
], |
||||
|
imports: [ |
||||
|
CommonModule, |
||||
|
SharedModule, |
||||
|
ProfileRoutingModule |
||||
|
] |
||||
|
}) |
||||
|
export class ProfileModule { } |
||||
@ -0,0 +1,80 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2019 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { Injectable, NgModule } from '@angular/core'; |
||||
|
import { Resolve, RouterModule, Routes } from '@angular/router'; |
||||
|
|
||||
|
import { EntitiesTableComponent } from '@shared/components/entity/entities-table.component'; |
||||
|
import { Authority } from '@shared/models/authority.enum'; |
||||
|
import { TenantsTableConfigResolver } from '@modules/home/pages/tenant/tenants-table-config.resolver'; |
||||
|
import { ProfileComponent } from '@modules/home/pages/profile/profile.component'; |
||||
|
import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard'; |
||||
|
import { Customer } from '@shared/models/customer.model'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { forkJoin, Observable, throwError } from 'rxjs'; |
||||
|
import { getCurrentAuthUser } from '@core/auth/auth.selectors'; |
||||
|
import { catchError, finalize, map, tap } from 'rxjs/operators'; |
||||
|
import {UsersTableConfigResolver} from '../user/users-table-config.resolver'; |
||||
|
|
||||
|
const routes: Routes = [ |
||||
|
{ |
||||
|
path: 'tenants', |
||||
|
data: { |
||||
|
breadcrumb: { |
||||
|
label: 'tenant.tenants', |
||||
|
icon: 'supervisor_account' |
||||
|
} |
||||
|
}, |
||||
|
children: [ |
||||
|
{ |
||||
|
path: '', |
||||
|
component: EntitiesTableComponent, |
||||
|
data: { |
||||
|
auth: [Authority.SYS_ADMIN], |
||||
|
title: 'tenant.tenants' |
||||
|
}, |
||||
|
resolve: { |
||||
|
entitiesTableConfig: TenantsTableConfigResolver |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
path: ':tenantId/users', |
||||
|
component: EntitiesTableComponent, |
||||
|
data: { |
||||
|
auth: [Authority.SYS_ADMIN], |
||||
|
title: 'user.tenant-admins', |
||||
|
breadcrumb: { |
||||
|
label: 'user.tenant-admins', |
||||
|
icon: 'account_circle' |
||||
|
} |
||||
|
}, |
||||
|
resolve: { |
||||
|
entitiesTableConfig: UsersTableConfigResolver |
||||
|
} |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
]; |
||||
|
|
||||
|
@NgModule({ |
||||
|
imports: [RouterModule.forChild(routes)], |
||||
|
exports: [RouterModule], |
||||
|
providers: [ |
||||
|
TenantsTableConfigResolver |
||||
|
] |
||||
|
}) |
||||
|
export class TenantRoutingModule { } |
||||
@ -0,0 +1,61 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2019 The Thingsboard Authors |
||||
|
|
||||
|
Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
you may not use this file except in compliance with the License. |
||||
|
You may obtain a copy of the License at |
||||
|
|
||||
|
http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
|
||||
|
Unless required by applicable law or agreed to in writing, software |
||||
|
distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
See the License for the specific language governing permissions and |
||||
|
limitations under the License. |
||||
|
|
||||
|
--> |
||||
|
<div class="tb-details-buttons"> |
||||
|
<button mat-raised-button color="primary" |
||||
|
[disabled]="(isLoading$ | async)" |
||||
|
(click)="onEntityAction($event, 'manageTenantAdmins')" |
||||
|
[fxShow]="!isEdit"> |
||||
|
{{'tenant.manage-tenant-admins' | translate }} |
||||
|
</button> |
||||
|
<button mat-raised-button color="primary" |
||||
|
[disabled]="(isLoading$ | async)" |
||||
|
(click)="onEntityAction($event, 'delete')" |
||||
|
[fxShow]="!hideDelete() && !isEdit"> |
||||
|
{{'tenant.delete' | translate }} |
||||
|
</button> |
||||
|
<div fxLayout="row"> |
||||
|
<button mat-raised-button |
||||
|
ngxClipboard |
||||
|
(cbOnSuccess)="onTenantIdCopied($event)" |
||||
|
[cbContent]="entity?.id?.id" |
||||
|
[fxShow]="!isEdit"> |
||||
|
<mat-icon svgIcon="mdi:clipboard-arrow-left"></mat-icon> |
||||
|
<span translate>tenant.copyId</span> |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="mat-padding" fxLayout="column"> |
||||
|
<form #entityNgForm="ngForm" [formGroup]="entityForm"> |
||||
|
<fieldset [disabled]="(isLoading$ | async) || !isEdit"> |
||||
|
<mat-form-field class="mat-block"> |
||||
|
<mat-label translate>tenant.title</mat-label> |
||||
|
<input matInput formControlName="title" required/> |
||||
|
<mat-error *ngIf="entityForm.get('title').hasError('required')"> |
||||
|
{{ 'tenant.title-required' | translate }} |
||||
|
</mat-error> |
||||
|
</mat-form-field> |
||||
|
<div formGroupName="additionalInfo" fxLayout="column"> |
||||
|
<mat-form-field class="mat-block"> |
||||
|
<mat-label translate>tenant.description</mat-label> |
||||
|
<textarea matInput formControlName="description" rows="2"></textarea> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
<tb-contact [parentForm]="entityForm" [isEdit]="isEdit"></tb-contact> |
||||
|
</fieldset> |
||||
|
</form> |
||||
|
</div> |
||||
@ -0,0 +1,76 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2019 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { Component } from '@angular/core'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; |
||||
|
import { Customer } from '@shared/models/customer.model'; |
||||
|
import { ContactBasedComponent } from '@shared/components/entity/contact-based.component'; |
||||
|
import {Tenant} from '@app/shared/models/tenant.model'; |
||||
|
import {ActionNotificationShow} from '@app/core/notification/notification.actions'; |
||||
|
import {TranslateService} from '@ngx-translate/core'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-tenant', |
||||
|
templateUrl: './tenant.component.html' |
||||
|
}) |
||||
|
export class TenantComponent extends ContactBasedComponent<Tenant> { |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
protected translate: TranslateService, |
||||
|
protected fb: FormBuilder) { |
||||
|
super(store, fb); |
||||
|
} |
||||
|
|
||||
|
hideDelete() { |
||||
|
if (this.entitiesTableConfig) { |
||||
|
return !this.entitiesTableConfig.deleteEnabled(this.entity); |
||||
|
} else { |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
buildEntityForm(entity: Tenant): FormGroup { |
||||
|
return this.fb.group( |
||||
|
{ |
||||
|
title: [entity ? entity.title : '', [Validators.required]], |
||||
|
additionalInfo: this.fb.group( |
||||
|
{ |
||||
|
description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''] |
||||
|
} |
||||
|
) |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
updateEntityForm(entity: Tenant) { |
||||
|
this.entityForm.patchValue({title: entity.title}); |
||||
|
this.entityForm.patchValue({additionalInfo: {description: entity.additionalInfo ? entity.additionalInfo.description : ''}}); |
||||
|
} |
||||
|
|
||||
|
onTenantIdCopied(event) { |
||||
|
this.store.dispatch(new ActionNotificationShow( |
||||
|
{ |
||||
|
message: this.translate.instant('tenant.idCopiedMessage'), |
||||
|
type: 'success', |
||||
|
duration: 750, |
||||
|
verticalPosition: 'bottom', |
||||
|
horizontalPosition: 'right' |
||||
|
})); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,36 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2019 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { NgModule } from '@angular/core'; |
||||
|
import { CommonModule } from '@angular/common'; |
||||
|
import { SharedModule } from '@shared/shared.module'; |
||||
|
import {TenantComponent} from '@modules/home/pages/tenant/tenant.component'; |
||||
|
import {TenantRoutingModule} from '@modules/home/pages/tenant/tenant-routing.module'; |
||||
|
|
||||
|
@NgModule({ |
||||
|
entryComponents: [ |
||||
|
TenantComponent |
||||
|
], |
||||
|
declarations: [ |
||||
|
TenantComponent |
||||
|
], |
||||
|
imports: [ |
||||
|
CommonModule, |
||||
|
SharedModule, |
||||
|
TenantRoutingModule |
||||
|
] |
||||
|
}) |
||||
|
export class TenantModule { } |
||||
@ -0,0 +1,105 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2019 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { Injectable } from '@angular/core'; |
||||
|
|
||||
|
import { Resolve, Router } from '@angular/router'; |
||||
|
|
||||
|
import { Tenant } from '@shared/models/tenant.model'; |
||||
|
import { |
||||
|
DateEntityTableColumn, |
||||
|
EntityTableColumn, |
||||
|
EntityTableConfig |
||||
|
} from '@shared/components/entity/entities-table-config.models'; |
||||
|
import { TenantService } from '@core/http/tenant.service'; |
||||
|
import { TranslateService } from '@ngx-translate/core'; |
||||
|
import { DatePipe } from '@angular/common'; |
||||
|
import { |
||||
|
EntityType, |
||||
|
entityTypeResources, |
||||
|
entityTypeTranslations |
||||
|
} from '@shared/models/entity-type.models'; |
||||
|
import { TenantComponent } from '@modules/home/pages/tenant/tenant.component'; |
||||
|
import { EntityAction } from '@shared/components/entity/entity-component.models'; |
||||
|
import { User } from '@shared/models/user.model'; |
||||
|
|
||||
|
@Injectable() |
||||
|
export class TenantsTableConfigResolver implements Resolve<EntityTableConfig<Tenant>> { |
||||
|
|
||||
|
private readonly config: EntityTableConfig<Tenant> = new EntityTableConfig<Tenant>(); |
||||
|
|
||||
|
constructor(private tenantService: TenantService, |
||||
|
private translate: TranslateService, |
||||
|
private datePipe: DatePipe, |
||||
|
private router: Router) { |
||||
|
|
||||
|
this.config.entityType = EntityType.CUSTOMER; |
||||
|
this.config.entityComponent = TenantComponent; |
||||
|
this.config.entityTranslations = entityTypeTranslations.get(EntityType.TENANT); |
||||
|
this.config.entityResources = entityTypeResources.get(EntityType.TENANT); |
||||
|
|
||||
|
this.config.columns.push( |
||||
|
new DateEntityTableColumn<Tenant>('createdTime', 'tenant.created-time', this.datePipe, '150px'), |
||||
|
new EntityTableColumn<Tenant>('title', 'tenant.title'), |
||||
|
new EntityTableColumn<Tenant>('email', 'contact.email'), |
||||
|
new EntityTableColumn<Tenant>('country', 'contact.country'), |
||||
|
new EntityTableColumn<Tenant>('city', 'contact.city') |
||||
|
); |
||||
|
|
||||
|
this.config.cellActionDescriptors.push( |
||||
|
{ |
||||
|
name: this.translate.instant('tenant.manage-tenant-admins'), |
||||
|
icon: 'account_circle', |
||||
|
isEnabled: () => true, |
||||
|
onAction: ($event, entity) => this.manageTenantAdmins($event, entity) |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
this.config.deleteEntityTitle = tenant => this.translate.instant('tenant.delete-tenant-title', { tenantTitle: tenant.title }); |
||||
|
this.config.deleteEntityContent = () => this.translate.instant('tenant.delete-tenant-text'); |
||||
|
this.config.deleteEntitiesTitle = count => this.translate.instant('tenant.delete-tenants-title', {count}); |
||||
|
this.config.deleteEntitiesContent = () => this.translate.instant('tenant.delete-tenants-text'); |
||||
|
|
||||
|
this.config.entitiesFetchFunction = pageLink => this.tenantService.getTenants(pageLink); |
||||
|
this.config.loadEntity = id => this.tenantService.getTenant(id.id); |
||||
|
this.config.saveEntity = tenant => this.tenantService.saveTenant(tenant); |
||||
|
this.config.deleteEntity = id => this.tenantService.deleteTenant(id.id); |
||||
|
this.config.onEntityAction = action => this.onTenantAction(action); |
||||
|
} |
||||
|
|
||||
|
resolve(): EntityTableConfig<Tenant> { |
||||
|
this.config.tableTitle = this.translate.instant('tenant.tenants'); |
||||
|
|
||||
|
return this.config; |
||||
|
} |
||||
|
|
||||
|
manageTenantAdmins($event: Event, tenant: Tenant) { |
||||
|
if ($event) { |
||||
|
$event.stopPropagation(); |
||||
|
} |
||||
|
this.router.navigateByUrl(`tenants/${tenant.id.id}/users`); |
||||
|
} |
||||
|
|
||||
|
onTenantAction(action: EntityAction<Tenant>): boolean { |
||||
|
switch (action.action) { |
||||
|
case 'manageTenantAdmins': |
||||
|
this.manageTenantAdmins(action.event, action.entity); |
||||
|
return true; |
||||
|
} |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,59 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2019 The Thingsboard Authors |
||||
|
|
||||
|
Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
you may not use this file except in compliance with the License. |
||||
|
You may obtain a copy of the License at |
||||
|
|
||||
|
http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
|
||||
|
Unless required by applicable law or agreed to in writing, software |
||||
|
distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
See the License for the specific language governing permissions and |
||||
|
limitations under the License. |
||||
|
|
||||
|
--> |
||||
|
<form style="min-width: 400px;"> |
||||
|
<mat-toolbar fxLayout="row" color="primary"> |
||||
|
<h2 translate>user.activation-link</h2> |
||||
|
<span fxFlex></span> |
||||
|
<button mat-button mat-icon-button |
||||
|
(click)="close()" |
||||
|
type="button"> |
||||
|
<mat-icon class="material-icons">close</mat-icon> |
||||
|
</button> |
||||
|
</mat-toolbar> |
||||
|
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> |
||||
|
</mat-progress-bar> |
||||
|
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div> |
||||
|
<div mat-dialog-content tb-toast toastTarget="activationLinkDialogContent"> |
||||
|
<div class="mat-content mat-padding" fxLayout="column"> |
||||
|
<span [innerHTML]="'user.activation-link-text' | translate: {activationLink: activationLink}"></span> |
||||
|
<div fxLayout="row" fxLayoutAlign="start center"> |
||||
|
<pre class="tb-highlight" fxFlex><code>{{ activationLink }}</code></pre> |
||||
|
<button mat-button mat-icon-button |
||||
|
color="primary" |
||||
|
ngxClipboard |
||||
|
cbContent="{{ activationLink }}" |
||||
|
(cbOnSuccess)="onActivationLinkCopied()" |
||||
|
matTooltip="{{ 'user.copy-activation-link' | translate }}" |
||||
|
matTooltipPosition="above"> |
||||
|
<mat-icon svgIcon="mdi:clipboard-arrow-left"></mat-icon> |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div mat-dialog-actions fxLayout="row"> |
||||
|
<span fxFlex></span> |
||||
|
<button mat-button color="primary" |
||||
|
style="margin-right: 20px;" |
||||
|
type="button" |
||||
|
cdkFocusInitial |
||||
|
[disabled]="(isLoading$ | async)" |
||||
|
(click)="close()"> |
||||
|
{{ 'action.ok' | translate }} |
||||
|
</button> |
||||
|
</div> |
||||
|
</form> |
||||
@ -0,0 +1,64 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2019 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { Component, Inject, OnInit } from '@angular/core'; |
||||
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material'; |
||||
|
import { PageComponent } from '@shared/components/page.component'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { TranslateService } from '@ngx-translate/core'; |
||||
|
import { ActionNotificationShow } from '@core/notification/notification.actions'; |
||||
|
|
||||
|
export interface ActivationLinkDialogData { |
||||
|
activationLink: string; |
||||
|
} |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-activation-link-dialog', |
||||
|
templateUrl: './activation-link-dialog.component.html' |
||||
|
}) |
||||
|
export class ActivationLinkDialogComponent extends PageComponent implements OnInit { |
||||
|
|
||||
|
activationLink: string; |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
@Inject(MAT_DIALOG_DATA) public data: ActivationLinkDialogData, |
||||
|
public dialogRef: MatDialogRef<ActivationLinkDialogComponent, void>, |
||||
|
private translate: TranslateService) { |
||||
|
super(store); |
||||
|
this.activationLink = this.data.activationLink; |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
} |
||||
|
|
||||
|
close(): void { |
||||
|
this.dialogRef.close(); |
||||
|
} |
||||
|
|
||||
|
onActivationLinkCopied() { |
||||
|
this.store.dispatch(new ActionNotificationShow( |
||||
|
{ |
||||
|
message: this.translate.instant('user.activation-link-copied-message'), |
||||
|
type: 'success', |
||||
|
target: 'activationLinkDialogContent', |
||||
|
duration: 1200, |
||||
|
verticalPosition: 'bottom', |
||||
|
horizontalPosition: 'left' |
||||
|
})); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,59 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2019 The Thingsboard Authors |
||||
|
|
||||
|
Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
you may not use this file except in compliance with the License. |
||||
|
You may obtain a copy of the License at |
||||
|
|
||||
|
http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
|
||||
|
Unless required by applicable law or agreed to in writing, software |
||||
|
distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
See the License for the specific language governing permissions and |
||||
|
limitations under the License. |
||||
|
|
||||
|
--> |
||||
|
<form (ngSubmit)="add()" style="width: 600px;"> |
||||
|
<mat-toolbar fxLayout="row" color="primary"> |
||||
|
<h2 translate>user.add</h2> |
||||
|
<span fxFlex></span> |
||||
|
<div [tb-help]="'user'"></div> |
||||
|
<button mat-button mat-icon-button |
||||
|
(click)="cancel()" |
||||
|
type="button"> |
||||
|
<mat-icon class="material-icons">close</mat-icon> |
||||
|
</button> |
||||
|
</mat-toolbar> |
||||
|
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> |
||||
|
</mat-progress-bar> |
||||
|
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div> |
||||
|
<div mat-dialog-content> |
||||
|
<tb-user></tb-user> |
||||
|
<mat-form-field class="mat-block"> |
||||
|
<mat-label translate>user.activation-method</mat-label> |
||||
|
<mat-select matInput [ngModelOptions]="{standalone: true}" [(ngModel)]="activationMethod"> |
||||
|
<mat-option *ngFor="let activationMethod of (activationMethods | enumToArray)" [value]="activationMethods[activationMethod]"> |
||||
|
{{ activationMethodTranslations.get(activationMethods[activationMethod]) | translate }} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
<div mat-dialog-actions fxLayout="row"> |
||||
|
<span fxFlex></span> |
||||
|
<button mat-button mat-raised-button color="primary" |
||||
|
type="submit" |
||||
|
[disabled]="(isLoading$ | async) || detailsForm.invalid || !detailsForm.dirty"> |
||||
|
{{ 'action.add' | translate }} |
||||
|
</button> |
||||
|
<button mat-button color="primary" |
||||
|
style="margin-right: 20px;" |
||||
|
type="button" |
||||
|
cdkFocusInitial |
||||
|
[disabled]="(isLoading$ | async)" |
||||
|
(click)="cancel()"> |
||||
|
{{ 'action.cancel' | translate }} |
||||
|
</button> |
||||
|
</div> |
||||
|
</form> |
||||
@ -0,0 +1,114 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2019 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { Component, Inject, OnInit, ViewChild } from '@angular/core'; |
||||
|
import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material'; |
||||
|
import { PageComponent } from '@shared/components/page.component'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { NgForm } from '@angular/forms'; |
||||
|
import { UserComponent } from '@modules/home/pages/user/user.component'; |
||||
|
import { Authority } from '@shared/models/authority.enum'; |
||||
|
import { ActivationMethod, activationMethodTranslations, User } from '@shared/models/user.model'; |
||||
|
import { CustomerId } from '@shared/models/id/customer-id'; |
||||
|
import { UserService } from '@core/http/user.service'; |
||||
|
import { Observable } from 'rxjs'; |
||||
|
import { |
||||
|
ActivationLinkDialogComponent, |
||||
|
ActivationLinkDialogData |
||||
|
} from '@modules/home/pages/user/activation-link-dialog.component'; |
||||
|
import {TenantId} from '@app/shared/models/id/tenant-id'; |
||||
|
|
||||
|
export interface AddUserDialogData { |
||||
|
tenantId: string; |
||||
|
customerId: string; |
||||
|
authority: Authority; |
||||
|
} |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-add-user-dialog', |
||||
|
templateUrl: './add-user-dialog.component.html' |
||||
|
}) |
||||
|
export class AddUserDialogComponent extends PageComponent implements OnInit { |
||||
|
|
||||
|
detailsForm: NgForm; |
||||
|
user: User; |
||||
|
|
||||
|
activationMethods = ActivationMethod; |
||||
|
|
||||
|
activationMethodTranslations = activationMethodTranslations; |
||||
|
|
||||
|
activationMethod = ActivationMethod.DISPLAY_ACTIVATION_LINK; |
||||
|
|
||||
|
@ViewChild(UserComponent, {static: true}) userComponent: UserComponent; |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
@Inject(MAT_DIALOG_DATA) public data: AddUserDialogData, |
||||
|
public dialogRef: MatDialogRef<AddUserDialogComponent, User>, |
||||
|
private userService: UserService, |
||||
|
private dialog: MatDialog) { |
||||
|
super(store); |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
this.user = {} as User; |
||||
|
this.userComponent.isEdit = true; |
||||
|
this.userComponent.entity = this.user; |
||||
|
this.detailsForm = this.userComponent.entityNgForm; |
||||
|
} |
||||
|
|
||||
|
cancel(): void { |
||||
|
this.dialogRef.close(null); |
||||
|
} |
||||
|
|
||||
|
add(): void { |
||||
|
if (this.detailsForm.valid) { |
||||
|
this.user = {...this.user, ...this.userComponent.entityForm.value}; |
||||
|
this.user.authority = this.data.authority; |
||||
|
this.user.tenantId = new TenantId(this.data.tenantId); |
||||
|
this.user.customerId = new CustomerId(this.data.customerId); |
||||
|
const sendActivationEmail = this.activationMethod === ActivationMethod.SEND_ACTIVATION_MAIL; |
||||
|
this.userService.saveUser(this.user, sendActivationEmail).subscribe( |
||||
|
(user) => { |
||||
|
if (this.activationMethod === ActivationMethod.DISPLAY_ACTIVATION_LINK) { |
||||
|
this.userService.getActivationLink(user.id.id).subscribe( |
||||
|
(activationLink) => { |
||||
|
this.displayActivationLink(activationLink).subscribe( |
||||
|
() => { |
||||
|
this.dialogRef.close(user); |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
); |
||||
|
} else { |
||||
|
this.dialogRef.close(user); |
||||
|
} |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
displayActivationLink(activationLink: string): Observable<void> { |
||||
|
return this.dialog.open<ActivationLinkDialogComponent, ActivationLinkDialogData, |
||||
|
void>(ActivationLinkDialogComponent, { |
||||
|
disableClose: true, |
||||
|
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], |
||||
|
data: { |
||||
|
activationLink |
||||
|
} |
||||
|
}).afterClosed(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,28 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2019 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { NgModule } from '@angular/core'; |
||||
|
import { RouterModule, Routes } from '@angular/router'; |
||||
|
import { UsersTableConfigResolver } from '@modules/home/pages/user/users-table-config.resolver'; |
||||
|
|
||||
|
@NgModule({ |
||||
|
imports: [], |
||||
|
exports: [RouterModule], |
||||
|
providers: [ |
||||
|
UsersTableConfigResolver |
||||
|
] |
||||
|
}) |
||||
|
export class UserRoutingModule { } |
||||
@ -0,0 +1,90 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2019 The Thingsboard Authors |
||||
|
|
||||
|
Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
you may not use this file except in compliance with the License. |
||||
|
You may obtain a copy of the License at |
||||
|
|
||||
|
http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
|
||||
|
Unless required by applicable law or agreed to in writing, software |
||||
|
distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
See the License for the specific language governing permissions and |
||||
|
limitations under the License. |
||||
|
|
||||
|
--> |
||||
|
<div class="tb-details-buttons"> |
||||
|
<button mat-raised-button color="primary" |
||||
|
[disabled]="(isLoading$ | async)" |
||||
|
(click)="onEntityAction($event, 'displayActivationLink')" |
||||
|
[fxShow]="!isEdit"> |
||||
|
{{'user.display-activation-link' | translate }} |
||||
|
</button> |
||||
|
<button mat-raised-button color="primary" |
||||
|
[disabled]="(isLoading$ | async)" |
||||
|
(click)="onEntityAction($event, 'resendActivation')" |
||||
|
[fxShow]="!isEdit"> |
||||
|
{{'user.resend-activation' | translate }} |
||||
|
</button> |
||||
|
<button mat-raised-button color="primary" |
||||
|
[disabled]="(isLoading$ | async)" |
||||
|
(click)="onEntityAction($event, 'loginAsUser')" |
||||
|
*ngIf="loginAsUserEnabled$ | async" |
||||
|
[fxShow]="!isEdit"> |
||||
|
{{ (entity?.authority === authority.TENANT_ADMIN ? 'user.login-as-tenant-admin' : 'user.login-as-customer-user') | translate }} |
||||
|
</button> |
||||
|
<button mat-raised-button color="primary" |
||||
|
[disabled]="(isLoading$ | async)" |
||||
|
(click)="onEntityAction($event, 'delete')" |
||||
|
[fxShow]="!hideDelete() && !isEdit"> |
||||
|
{{'user.delete' | translate }} |
||||
|
</button> |
||||
|
</div> |
||||
|
<div class="mat-padding" fxLayout="column"> |
||||
|
<form #entityNgForm="ngForm" [formGroup]="entityForm"> |
||||
|
<fieldset [disabled]="(isLoading$ | async) || !isEdit"> |
||||
|
<mat-form-field class="mat-block"> |
||||
|
<mat-label translate>user.email</mat-label> |
||||
|
<input matInput formControlName="email" required> |
||||
|
<mat-error *ngIf="entityForm.get('email').hasError('email')"> |
||||
|
{{ 'user.invalid-email-format' | translate }} |
||||
|
</mat-error> |
||||
|
<mat-error *ngIf="entityForm.get('email').hasError('required')"> |
||||
|
{{ 'user.email-required' | translate }} |
||||
|
</mat-error> |
||||
|
</mat-form-field> |
||||
|
<mat-form-field class="mat-block"> |
||||
|
<mat-label translate>user.first-name</mat-label> |
||||
|
<input matInput formControlName="firstName"> |
||||
|
</mat-form-field> |
||||
|
<mat-form-field class="mat-block"> |
||||
|
<mat-label translate>user.last-name</mat-label> |
||||
|
<input matInput formControlName="lastName"> |
||||
|
</mat-form-field> |
||||
|
<div formGroupName="additionalInfo" fxLayout="column"> |
||||
|
<mat-form-field class="mat-block"> |
||||
|
<mat-label translate>user.description</mat-label> |
||||
|
<textarea matInput formControlName="description" rows="2"></textarea> |
||||
|
</mat-form-field> |
||||
|
<section class="tb-default-dashboard" fxFlex fxLayout="column" *ngIf="entity?.id"> |
||||
|
<section fxFlex fxLayout="column" fxLayout.gt-sm="row"> |
||||
|
<tb-dashboard-autocomplete |
||||
|
fxFlex |
||||
|
placeholder="{{ 'user.default-dashboard' | translate }}" |
||||
|
formControlName="defaultDashboardId" |
||||
|
[dashboardsScope]="entity?.authority === authority.TENANT_ADMIN ? 'tenant' : 'customer'" |
||||
|
[tenantId]="entity?.tenantId?.id" |
||||
|
[customerId]="entity?.customerId?.id" |
||||
|
[selectFirstDashboard]="false" |
||||
|
></tb-dashboard-autocomplete> |
||||
|
<mat-checkbox fxFlex formControlName="defaultDashboardFullscreen"> |
||||
|
{{ 'user.always-fullscreen' | translate }} |
||||
|
</mat-checkbox> |
||||
|
</section> |
||||
|
</section> |
||||
|
</div> |
||||
|
</fieldset> |
||||
|
</form> |
||||
|
</div> |
||||
@ -0,0 +1,35 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2019 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
@import "../../../../../scss/constants"; |
||||
|
|
||||
|
:host { |
||||
|
.tb-default-dashboard { |
||||
|
tb-dashboard-autocomplete { |
||||
|
@media #{$mat-gt-sm} { |
||||
|
padding-right: 12px; |
||||
|
} |
||||
|
|
||||
|
@media #{$mat-lt-md} { |
||||
|
padding-bottom: 12px; |
||||
|
} |
||||
|
} |
||||
|
mat-checkbox { |
||||
|
@media #{$mat-gt-sm} { |
||||
|
margin-top: 16px; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,82 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2019 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { Component, OnInit } from '@angular/core'; |
||||
|
import { select, Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { EntityComponent } from '@shared/components/entity/entity.component'; |
||||
|
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; |
||||
|
import { User } from '@shared/models/user.model'; |
||||
|
import { selectAuth, selectUserDetails } from '@core/auth/auth.selectors'; |
||||
|
import { map } from 'rxjs/operators'; |
||||
|
import { Authority } from '@shared/models/authority.enum'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-user', |
||||
|
templateUrl: './user.component.html', |
||||
|
styleUrls: ['./user.component.scss'] |
||||
|
}) |
||||
|
export class UserComponent extends EntityComponent<User> { |
||||
|
|
||||
|
authority = Authority; |
||||
|
|
||||
|
loginAsUserEnabled$ = this.store.pipe( |
||||
|
select(selectAuth), |
||||
|
map((auth) => auth.userTokenAccessEnabled) |
||||
|
); |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
public fb: FormBuilder) { |
||||
|
super(store); |
||||
|
} |
||||
|
|
||||
|
hideDelete() { |
||||
|
if (this.entitiesTableConfig) { |
||||
|
return !this.entitiesTableConfig.deleteEnabled(this.entity); |
||||
|
} else { |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
buildForm(entity: User): FormGroup { |
||||
|
return this.fb.group( |
||||
|
{ |
||||
|
email: [entity ? entity.email : '', [Validators.required, Validators.email]], |
||||
|
firstName: [entity ? entity.firstName : ''], |
||||
|
lastName: [entity ? entity.lastName : ''], |
||||
|
additionalInfo: this.fb.group( |
||||
|
{ |
||||
|
description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''], |
||||
|
defaultDashboardId: [entity && entity.additionalInfo ? entity.additionalInfo.defaultDashboardId : null], |
||||
|
defaultDashboardFullscreen: [entity && entity.additionalInfo ? entity.additionalInfo.defaultDashboardFullscreen : false], |
||||
|
} |
||||
|
) |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
updateForm(entity: User) { |
||||
|
this.entityForm.patchValue({email: entity.email}); |
||||
|
this.entityForm.patchValue({firstName: entity.firstName}); |
||||
|
this.entityForm.patchValue({lastName: entity.lastName}); |
||||
|
this.entityForm.patchValue({additionalInfo: {description: entity.additionalInfo ? entity.additionalInfo.description : ''}}); |
||||
|
this.entityForm.patchValue({additionalInfo: |
||||
|
{defaultDashboardId: entity.additionalInfo ? entity.additionalInfo.defaultDashboardId : null}}); |
||||
|
this.entityForm.patchValue({additionalInfo: |
||||
|
{defaultDashboardFullscreen: entity.additionalInfo ? entity.additionalInfo.defaultDashboardFullscreen : false}}); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,42 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2019 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { NgModule } from '@angular/core'; |
||||
|
import { CommonModule } from '@angular/common'; |
||||
|
import { SharedModule } from '@shared/shared.module'; |
||||
|
import { UserComponent } from '@modules/home/pages/user/user.component'; |
||||
|
import { UserRoutingModule } from '@modules/home/pages/user/user-routing.module'; |
||||
|
import { AddUserDialogComponent } from '@modules/home/pages/user/add-user-dialog.component'; |
||||
|
import { ActivationLinkDialogComponent } from '@modules/home/pages/user/activation-link-dialog.component'; |
||||
|
|
||||
|
@NgModule({ |
||||
|
entryComponents: [ |
||||
|
UserComponent, |
||||
|
AddUserDialogComponent, |
||||
|
ActivationLinkDialogComponent |
||||
|
], |
||||
|
declarations: [ |
||||
|
UserComponent, |
||||
|
AddUserDialogComponent, |
||||
|
ActivationLinkDialogComponent |
||||
|
], |
||||
|
imports: [ |
||||
|
CommonModule, |
||||
|
SharedModule, |
||||
|
UserRoutingModule |
||||
|
] |
||||
|
}) |
||||
|
export class UserModule { } |
||||
@ -0,0 +1,230 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2019 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { Injectable } from '@angular/core'; |
||||
|
|
||||
|
import { ActivatedRouteSnapshot, Resolve } from '@angular/router'; |
||||
|
import { |
||||
|
DateEntityTableColumn, |
||||
|
EntityTableColumn, |
||||
|
EntityTableConfig |
||||
|
} from '@shared/components/entity/entities-table-config.models'; |
||||
|
import { TranslateService } from '@ngx-translate/core'; |
||||
|
import { DatePipe } from '@angular/common'; |
||||
|
import { |
||||
|
EntityType, |
||||
|
entityTypeResources, |
||||
|
entityTypeTranslations |
||||
|
} from '@shared/models/entity-type.models'; |
||||
|
import { User } from '@shared/models/user.model'; |
||||
|
import { UserService } from '@core/http/user.service'; |
||||
|
import { UserComponent } from '@modules/home/pages/user/user.component'; |
||||
|
import { CustomerService } from '@core/http/customer.service'; |
||||
|
import { map, mergeMap, take, tap } from 'rxjs/operators'; |
||||
|
import { forkJoin, noop, Observable, of } from 'rxjs'; |
||||
|
import { Authority } from '@shared/models/authority.enum'; |
||||
|
import { CustomerId } from '@shared/models/id/customer-id'; |
||||
|
import { MatDialog } from '@angular/material'; |
||||
|
import { EntityAction } from '@shared/components/entity/entity-component.models'; |
||||
|
import { |
||||
|
AddUserDialogComponent, |
||||
|
AddUserDialogData |
||||
|
} from '@modules/home/pages/user/add-user-dialog.component'; |
||||
|
import { AuthState } from '@core/auth/auth.models'; |
||||
|
import { select, Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { selectAuth } from '@core/auth/auth.selectors'; |
||||
|
import { AuthService } from '@core/auth/auth.service'; |
||||
|
import { |
||||
|
ActivationLinkDialogComponent, |
||||
|
ActivationLinkDialogData |
||||
|
} from '@modules/home/pages/user/activation-link-dialog.component'; |
||||
|
import { ActionNotificationShow } from '@core/notification/notification.actions'; |
||||
|
import { NULL_UUID } from '@shared/models/id/has-uuid'; |
||||
|
import { Customer } from '@shared/models/customer.model'; |
||||
|
import {TenantService} from '@app/core/http/tenant.service'; |
||||
|
import {TenantId} from '@app/shared/models/id/tenant-id'; |
||||
|
|
||||
|
export interface UsersTableRouteData { |
||||
|
authority: Authority; |
||||
|
} |
||||
|
|
||||
|
@Injectable() |
||||
|
export class UsersTableConfigResolver implements Resolve<EntityTableConfig<User>> { |
||||
|
|
||||
|
private readonly config: EntityTableConfig<User> = new EntityTableConfig<User>(); |
||||
|
|
||||
|
private tenantId: string; |
||||
|
private customerId: string; |
||||
|
private authority: Authority; |
||||
|
private authUser: User; |
||||
|
|
||||
|
constructor(private store: Store<AppState>, |
||||
|
private userService: UserService, |
||||
|
private authService: AuthService, |
||||
|
private tenantService: TenantService, |
||||
|
private customerService: CustomerService, |
||||
|
private translate: TranslateService, |
||||
|
private datePipe: DatePipe, |
||||
|
private dialog: MatDialog) { |
||||
|
|
||||
|
this.config.entityType = EntityType.USER; |
||||
|
this.config.entityComponent = UserComponent; |
||||
|
this.config.entityTranslations = entityTypeTranslations.get(EntityType.USER); |
||||
|
this.config.entityResources = entityTypeResources.get(EntityType.USER); |
||||
|
|
||||
|
this.config.columns.push( |
||||
|
new DateEntityTableColumn<User>('createdTime', 'user.created-time', this.datePipe, '150px'), |
||||
|
new EntityTableColumn<User>('firstName', 'user.first-name'), |
||||
|
new EntityTableColumn<User>('lastName', 'user.last-name'), |
||||
|
new EntityTableColumn<User>('email', 'user.email') |
||||
|
); |
||||
|
|
||||
|
this.config.deleteEnabled = user => user && user.id && user.id.id !== this.authUser.id.id; |
||||
|
this.config.deleteEntityTitle = user => this.translate.instant('user.delete-user-title', { userEmail: user.email }); |
||||
|
this.config.deleteEntityContent = () => this.translate.instant('user.delete-user-text'); |
||||
|
this.config.deleteEntitiesTitle = count => this.translate.instant('user.delete-users-title', {count}); |
||||
|
this.config.deleteEntitiesContent = () => this.translate.instant('user.delete-users-text'); |
||||
|
|
||||
|
this.config.loadEntity = id => this.userService.getUser(id.id); |
||||
|
this.config.saveEntity = user => this.saveUser(user); |
||||
|
this.config.deleteEntity = id => this.userService.deleteUser(id.id); |
||||
|
this.config.onEntityAction = action => this.onUserAction(action); |
||||
|
this.config.addEntity = () => this.addUser(); |
||||
|
} |
||||
|
|
||||
|
resolve(route: ActivatedRouteSnapshot): Observable<EntityTableConfig<User>> { |
||||
|
const routeParams = route.params; |
||||
|
return this.store.pipe(select(selectAuth), take(1)).pipe( |
||||
|
tap((auth) => { |
||||
|
this.authUser = auth.userDetails; |
||||
|
this.authority = routeParams.tenantId ? Authority.TENANT_ADMIN : Authority.CUSTOMER_USER; |
||||
|
if (this.authority === Authority.TENANT_ADMIN) { |
||||
|
this.tenantId = routeParams.tenantId; |
||||
|
this.customerId = NULL_UUID; |
||||
|
this.config.entitiesFetchFunction = pageLink => this.userService.getTenantAdmins(this.tenantId, pageLink); |
||||
|
} else { |
||||
|
this.tenantId = this.authUser.tenantId.id; |
||||
|
this.customerId = routeParams.customerId; |
||||
|
this.config.entitiesFetchFunction = pageLink => this.userService.getCustomerUsers(this.customerId, pageLink); |
||||
|
} |
||||
|
this.updateActionCellDescriptors(auth); |
||||
|
}), |
||||
|
mergeMap(() => this.authority === Authority.TENANT_ADMIN ? |
||||
|
this.tenantService.getTenant(this.tenantId) : |
||||
|
this.customerService.getCustomer(this.customerId)), |
||||
|
map((parentEntity) => { |
||||
|
if (this.authority === Authority.TENANT_ADMIN) { |
||||
|
this.config.tableTitle = parentEntity.title + ': ' + this.translate.instant('user.tenant-admins'); |
||||
|
} else { |
||||
|
this.config.tableTitle = parentEntity.title + ': ' + this.translate.instant('user.customer-users'); |
||||
|
} |
||||
|
return this.config; |
||||
|
}) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
updateActionCellDescriptors(auth: AuthState) { |
||||
|
this.config.cellActionDescriptors.splice(0); |
||||
|
if (auth.userTokenAccessEnabled) { |
||||
|
this.config.cellActionDescriptors.push( |
||||
|
{ |
||||
|
name: this.authority === Authority.TENANT_ADMIN ? |
||||
|
this.translate.instant('user.login-as-tenant-admin') : |
||||
|
this.translate.instant('user.login-as-customer-user'), |
||||
|
icon: 'mdi:login', |
||||
|
isMdiIcon: true, |
||||
|
isEnabled: () => true, |
||||
|
onAction: ($event, entity) => this.loginAsUser($event, entity) |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
saveUser(user: User): Observable<User> { |
||||
|
user.tenantId = new TenantId(this.tenantId); |
||||
|
user.customerId = new CustomerId(this.customerId); |
||||
|
user.authority = this.authority; |
||||
|
return this.userService.saveUser(user); |
||||
|
} |
||||
|
|
||||
|
addUser(): Observable<User> { |
||||
|
return this.dialog.open<AddUserDialogComponent, AddUserDialogData, |
||||
|
User>(AddUserDialogComponent, { |
||||
|
disableClose: true, |
||||
|
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], |
||||
|
data: { |
||||
|
tenantId: this.tenantId, |
||||
|
customerId: this.customerId, |
||||
|
authority: this.authority |
||||
|
} |
||||
|
}).afterClosed(); |
||||
|
} |
||||
|
|
||||
|
loginAsUser($event: Event, user: User) { |
||||
|
if ($event) { |
||||
|
$event.stopPropagation(); |
||||
|
} |
||||
|
this.authService.loginAsUser(user.id.id).subscribe(); |
||||
|
} |
||||
|
|
||||
|
displayActivationLink($event: Event, user: User) { |
||||
|
if ($event) { |
||||
|
$event.stopPropagation(); |
||||
|
} |
||||
|
this.userService.getActivationLink(user.id.id).subscribe( |
||||
|
(activationLink) => { |
||||
|
this.dialog.open<ActivationLinkDialogComponent, ActivationLinkDialogData, |
||||
|
void>(ActivationLinkDialogComponent, { |
||||
|
disableClose: true, |
||||
|
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], |
||||
|
data: { |
||||
|
activationLink |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
resendActivation($event: Event, user: User) { |
||||
|
if ($event) { |
||||
|
$event.stopPropagation(); |
||||
|
} |
||||
|
this.userService.sendActivationEmail(user.email).subscribe(() => { |
||||
|
this.store.dispatch(new ActionNotificationShow( |
||||
|
{ |
||||
|
message: this.translate.instant('user.activation-email-sent-message'), |
||||
|
type: 'success' |
||||
|
})); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
onUserAction(action: EntityAction<User>): boolean { |
||||
|
switch (action.action) { |
||||
|
case 'loginAsUser': |
||||
|
this.loginAsUser(action.event, action.entity); |
||||
|
return true; |
||||
|
case 'displayActivationLink': |
||||
|
this.displayActivationLink(action.event, action.entity); |
||||
|
return true; |
||||
|
case 'resendActivation': |
||||
|
this.resendActivation(action.event, action.entity); |
||||
|
return true; |
||||
|
} |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,63 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2019 The Thingsboard Authors |
||||
|
|
||||
|
Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
you may not use this file except in compliance with the License. |
||||
|
You may obtain a copy of the License at |
||||
|
|
||||
|
http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
|
||||
|
Unless required by applicable law or agreed to in writing, software |
||||
|
distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
See the License for the specific language governing permissions and |
||||
|
limitations under the License. |
||||
|
|
||||
|
--> |
||||
|
<section [formGroup]="parentForm"> |
||||
|
<mat-form-field class="mat-block"> |
||||
|
<mat-label translate>contact.country</mat-label> |
||||
|
<mat-select matInput formControlName="country"> |
||||
|
<mat-option *ngFor="let country of countries" [value]="country"> |
||||
|
{{ country }} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
<div fxLayout.gt-sm="row" fxLayoutGap.gt-sm="10px"> |
||||
|
<mat-form-field class="mat-block"> |
||||
|
<mat-label translate>contact.city</mat-label> |
||||
|
<input matInput formControlName="city"> |
||||
|
</mat-form-field> |
||||
|
<mat-form-field class="mat-block"> |
||||
|
<mat-label translate>contact.state</mat-label> |
||||
|
<input matInput formControlName="state"> |
||||
|
</mat-form-field> |
||||
|
<mat-form-field class="mat-block"> |
||||
|
<mat-label translate>contact.postal-code</mat-label> |
||||
|
<input matInput formControlName="zip"> |
||||
|
<mat-error *ngIf="parentForm.get('zip').hasError('pattern')"> |
||||
|
{{ 'contact.postal-code-invalid' | translate }} |
||||
|
</mat-error> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
<mat-form-field class="mat-block"> |
||||
|
<mat-label translate>contact.address</mat-label> |
||||
|
<input matInput formControlName="address"> |
||||
|
</mat-form-field> |
||||
|
<mat-form-field class="mat-block"> |
||||
|
<mat-label translate>contact.address2</mat-label> |
||||
|
<input matInput formControlName="address2"> |
||||
|
</mat-form-field> |
||||
|
<mat-form-field class="mat-block"> |
||||
|
<mat-label translate>contact.phone</mat-label> |
||||
|
<input matInput formControlName="phone"> |
||||
|
</mat-form-field> |
||||
|
<mat-form-field class="mat-block"> |
||||
|
<mat-label translate>contact.email</mat-label> |
||||
|
<input matInput formControlName="email"> |
||||
|
<mat-error *ngIf="parentForm.get('email').hasError('email')"> |
||||
|
{{ 'user.invalid-email-format' | translate }} |
||||
|
</mat-error> |
||||
|
</mat-form-field> |
||||
|
</section> |
||||
@ -0,0 +1,34 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2019 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { Component, Input } from '@angular/core'; |
||||
|
import { FormGroup } from '@angular/forms'; |
||||
|
import { COUNTRIES } from '@shared/components/contact.models'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-contact', |
||||
|
templateUrl: './contact.component.html' |
||||
|
}) |
||||
|
export class ContactComponent { |
||||
|
|
||||
|
@Input() |
||||
|
parentForm: FormGroup; |
||||
|
|
||||
|
@Input() isEdit: boolean; |
||||
|
|
||||
|
countries = COUNTRIES; |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,291 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2019 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
export const COUNTRIES = [ |
||||
|
'Afghanistan', |
||||
|
'Åland Islands', |
||||
|
'Albania', |
||||
|
'Algeria', |
||||
|
'American Samoa', |
||||
|
'Andorra', |
||||
|
'Angola', |
||||
|
'Anguilla', |
||||
|
'Antarctica', |
||||
|
'Antigua and Barbuda', |
||||
|
'Argentina', |
||||
|
'Armenia', |
||||
|
'Aruba', |
||||
|
'Australia', |
||||
|
'Austria', |
||||
|
'Azerbaijan', |
||||
|
'Bahamas', |
||||
|
'Bahrain', |
||||
|
'Bangladesh', |
||||
|
'Barbados', |
||||
|
'Belarus', |
||||
|
'Belgium', |
||||
|
'Belize', |
||||
|
'Benin', |
||||
|
'Bermuda', |
||||
|
'Bhutan', |
||||
|
'Bolivia', |
||||
|
'Bonaire, Sint Eustatius and Saba', |
||||
|
'Bosnia and Herzegovina', |
||||
|
'Botswana', |
||||
|
'Bouvet Island', |
||||
|
'Brazil', |
||||
|
'British Indian Ocean Territory', |
||||
|
'Brunei Darussalam', |
||||
|
'Bulgaria', |
||||
|
'Burkina Faso', |
||||
|
'Burundi', |
||||
|
'Cambodia', |
||||
|
'Cameroon', |
||||
|
'Canada', |
||||
|
'Cape Verde', |
||||
|
'Cayman Islands', |
||||
|
'Central African Republic', |
||||
|
'Chad', |
||||
|
'Chile', |
||||
|
'China', |
||||
|
'Christmas Island', |
||||
|
'Cocos (Keeling) Islands', |
||||
|
'Colombia', |
||||
|
'Comoros', |
||||
|
'Congo', |
||||
|
'Congo, The Democratic Republic of the', |
||||
|
'Cook Islands', |
||||
|
'Costa Rica', |
||||
|
'Côte d\'Ivoire', |
||||
|
'Croatia', |
||||
|
'Cuba', |
||||
|
'Curaçao', |
||||
|
'Cyprus', |
||||
|
'Czech Republic', |
||||
|
'Denmark', |
||||
|
'Djibouti', |
||||
|
'Dominica', |
||||
|
'Dominican Republic', |
||||
|
'Ecuador', |
||||
|
'Egypt', |
||||
|
'El Salvador', |
||||
|
'Equatorial Guinea', |
||||
|
'Eritrea', |
||||
|
'Estonia', |
||||
|
'Ethiopia', |
||||
|
'Falkland Islands (Malvinas)', |
||||
|
'Faroe Islands', |
||||
|
'Fiji', |
||||
|
'Finland', |
||||
|
'France', |
||||
|
'French Guiana', |
||||
|
'French Polynesia', |
||||
|
'French Southern Territories', |
||||
|
'Gabon', |
||||
|
'Gambia', |
||||
|
'Georgia', |
||||
|
'Germany', |
||||
|
'Ghana', |
||||
|
'Gibraltar', |
||||
|
'Greece', |
||||
|
'Greenland', |
||||
|
'Grenada', |
||||
|
'Guadeloupe', |
||||
|
'Guam', |
||||
|
'Guatemala', |
||||
|
'Guernsey', |
||||
|
'Guinea', |
||||
|
'Guinea-Bissau', |
||||
|
'Guyana', |
||||
|
'Haiti', |
||||
|
'Heard Island and McDonald Islands', |
||||
|
'Holy See (Vatican City State)', |
||||
|
'Honduras', |
||||
|
'Hong Kong', |
||||
|
'Hungary', |
||||
|
'Iceland', |
||||
|
'India', |
||||
|
'Indonesia', |
||||
|
'Iran, Islamic Republic of', |
||||
|
'Iraq', |
||||
|
'Ireland', |
||||
|
'Isle of Man', |
||||
|
'Israel', |
||||
|
'Italy', |
||||
|
'Jamaica', |
||||
|
'Japan', |
||||
|
'Jersey', |
||||
|
'Jordan', |
||||
|
'Kazakhstan', |
||||
|
'Kenya', |
||||
|
'Kiribati', |
||||
|
'Korea, Democratic People\'s Republic of', |
||||
|
'Korea, Republic of', |
||||
|
'Kuwait', |
||||
|
'Kyrgyzstan', |
||||
|
'Lao People\'s Democratic Republic', |
||||
|
'Latvia', |
||||
|
'Lebanon', |
||||
|
'Lesotho', |
||||
|
'Liberia', |
||||
|
'Libya', |
||||
|
'Liechtenstein', |
||||
|
'Lithuania', |
||||
|
'Luxembourg', |
||||
|
'Macao', |
||||
|
'Macedonia, Republic Of', |
||||
|
'Madagascar', |
||||
|
'Malawi', |
||||
|
'Malaysia', |
||||
|
'Maldives', |
||||
|
'Mali', |
||||
|
'Malta', |
||||
|
'Marshall Islands', |
||||
|
'Martinique', |
||||
|
'Mauritania', |
||||
|
'Mauritius', |
||||
|
'Mayotte', |
||||
|
'Mexico', |
||||
|
'Micronesia, Federated States of', |
||||
|
'Moldova, Republic of', |
||||
|
'Monaco', |
||||
|
'Mongolia', |
||||
|
'Montenegro', |
||||
|
'Montserrat', |
||||
|
'Morocco', |
||||
|
'Mozambique', |
||||
|
'Myanmar', |
||||
|
'Namibia', |
||||
|
'Nauru', |
||||
|
'Nepal', |
||||
|
'Netherlands', |
||||
|
'New Caledonia', |
||||
|
'New Zealand', |
||||
|
'Nicaragua', |
||||
|
'Niger', |
||||
|
'Nigeria', |
||||
|
'Niue', |
||||
|
'Norfolk Island', |
||||
|
'Northern Mariana Islands', |
||||
|
'Norway', |
||||
|
'Oman', |
||||
|
'Pakistan', |
||||
|
'Palau', |
||||
|
'Palestinian Territory, Occupied', |
||||
|
'Panama', |
||||
|
'Papua New Guinea', |
||||
|
'Paraguay', |
||||
|
'Peru', |
||||
|
'Philippines', |
||||
|
'Pitcairn', |
||||
|
'Poland', |
||||
|
'Portugal', |
||||
|
'Puerto Rico', |
||||
|
'Qatar', |
||||
|
'Reunion', |
||||
|
'Romania', |
||||
|
'Russian Federation', |
||||
|
'Rwanda', |
||||
|
'Saint Barthélemy', |
||||
|
'Saint Helena, Ascension and Tristan da Cunha', |
||||
|
'Saint Kitts and Nevis', |
||||
|
'Saint Lucia', |
||||
|
'Saint Martin (French Part)', |
||||
|
'Saint Pierre and Miquelon', |
||||
|
'Saint Vincent and the Grenadines', |
||||
|
'Samoa', |
||||
|
'San Marino', |
||||
|
'Sao Tome and Principe', |
||||
|
'Saudi Arabia', |
||||
|
'Senegal', |
||||
|
'Serbia', |
||||
|
'Seychelles', |
||||
|
'Sierra Leone', |
||||
|
'Singapore', |
||||
|
'Sint Maarten (Dutch Part)', |
||||
|
'Slovakia', |
||||
|
'Slovenia', |
||||
|
'Solomon Islands', |
||||
|
'Somalia', |
||||
|
'South Africa', |
||||
|
'South Georgia and the South Sandwich Islands', |
||||
|
'South Sudan', |
||||
|
'Spain', |
||||
|
'Sri Lanka', |
||||
|
'Sudan', |
||||
|
'Suriname', |
||||
|
'Svalbard and Jan Mayen', |
||||
|
'Swaziland', |
||||
|
'Sweden', |
||||
|
'Switzerland', |
||||
|
'Syrian Arab Republic', |
||||
|
'Taiwan', |
||||
|
'Tajikistan', |
||||
|
'Tanzania, United Republic of', |
||||
|
'Thailand', |
||||
|
'Timor-Leste', |
||||
|
'Togo', |
||||
|
'Tokelau', |
||||
|
'Tonga', |
||||
|
'Trinidad and Tobago', |
||||
|
'Tunisia', |
||||
|
'Turkey', |
||||
|
'Turkmenistan', |
||||
|
'Turks and Caicos Islands', |
||||
|
'Tuvalu', |
||||
|
'Uganda', |
||||
|
'Ukraine', |
||||
|
'United Arab Emirates', |
||||
|
'United Kingdom', |
||||
|
'United States', |
||||
|
'United States Minor Outlying Islands', |
||||
|
'Uruguay', |
||||
|
'Uzbekistan', |
||||
|
'Vanuatu', |
||||
|
'Venezuela', |
||||
|
'Viet Nam', |
||||
|
'Virgin Islands, British', |
||||
|
'Virgin Islands, U.S.', |
||||
|
'Wallis and Futuna', |
||||
|
'Western Sahara', |
||||
|
'Yemen', |
||||
|
'Zambia', |
||||
|
'Zimbabwe' |
||||
|
]; |
||||
|
|
||||
|
/* tslint:disable */ |
||||
|
export const POSTAL_CODE_PATTERNS = { |
||||
|
'United States': '(\\d{5}([\\-]\\d{4})?)', |
||||
|
'Australia': '[0-9]{4}', |
||||
|
'Austria': '[0-9]{4}', |
||||
|
'Belgium': '[0-9]{4}', |
||||
|
'Brazil': '[0-9]{5}[\\-]?[0-9]{3}', |
||||
|
'Canada': '^(?!.*[DFIOQU])[A-VXY][0-9][A-Z][ -]?[0-9][A-Z][0-9]$', |
||||
|
'Denmark': '[0-9]{3,4}', |
||||
|
'Faroe Islands': '[0-9]{3,4}', |
||||
|
'Netherlands': '[1-9][0-9]{3}\\s?[a-zA-Z]{2}', |
||||
|
'Germany': '[0-9]{5}', |
||||
|
'Hungary': '[0-9]{4}', |
||||
|
'Italy': '[0-9]{5}', |
||||
|
'Japan': '\\d{3}-\\d{4}', |
||||
|
'Luxembourg': '(L\\s*(-|—|–))\\s*?[\\d]{4}', |
||||
|
'Poland': '[0-9]{2}\\-[0-9]{3}', |
||||
|
'Spain': '((0[1-9]|5[0-2])|[1-4][0-9])[0-9]{3}', |
||||
|
'Sweden': '\\d{3}\\s?\\d{2}', |
||||
|
'United Kingdom': '[A-Za-z]{1,2}[0-9Rr][0-9A-Za-z]? [0-9][ABD-HJLNP-UW-Zabd-hjlnp-uw-z]{2}' |
||||
|
}; |
||||
|
/* tslint:enable */ |
||||
|
|
||||
@ -0,0 +1,46 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2019 The Thingsboard Authors |
||||
|
|
||||
|
Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
you may not use this file except in compliance with the License. |
||||
|
You may obtain a copy of the License at |
||||
|
|
||||
|
http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
|
||||
|
Unless required by applicable law or agreed to in writing, software |
||||
|
distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
See the License for the specific language governing permissions and |
||||
|
limitations under the License. |
||||
|
|
||||
|
--> |
||||
|
<mat-form-field [formGroup]="selectDashboardFormGroup" class="mat-block"> |
||||
|
<input matInput type="text" placeholder="{{ placeholder || ('dashboard.dashboard' | translate) }}" |
||||
|
#dashboardInput |
||||
|
formControlName="dashboard" |
||||
|
[required]="required" |
||||
|
[matAutocomplete]="dashboardAutocomplete"> |
||||
|
<button *ngIf="selectDashboardFormGroup.get('dashboard').value && !disabled" |
||||
|
type="button" |
||||
|
matSuffix mat-button mat-icon-button aria-label="Clear" |
||||
|
(click)="clear()"> |
||||
|
<mat-icon class="material-icons">close</mat-icon> |
||||
|
</button> |
||||
|
<mat-autocomplete #dashboardAutocomplete="matAutocomplete" [displayWith]="displayDashboardFn"> |
||||
|
<mat-option *ngFor="let dashboard of filteredDashboards | async" [value]="dashboard"> |
||||
|
<span [innerHTML]="dashboard.title | highlight:searchText"></span> |
||||
|
</mat-option> |
||||
|
<mat-option *ngIf="!(filteredDashboards | async)?.length" [value]="null"> |
||||
|
<span> |
||||
|
{{ translate.get('dashboard.no-dashboards-matching', {entity: searchText}) | async }} |
||||
|
</span> |
||||
|
</mat-option> |
||||
|
</mat-autocomplete> |
||||
|
<mat-error> |
||||
|
<ng-content select="[tb-error]"></ng-content> |
||||
|
</mat-error> |
||||
|
<mat-hint> |
||||
|
<ng-content select="[tb-hint]"></ng-content> |
||||
|
</mat-hint> |
||||
|
</mat-form-field> |
||||
@ -0,0 +1,226 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2019 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import {AfterViewInit, Component, ElementRef, forwardRef, Input, OnInit, ViewChild} from '@angular/core'; |
||||
|
import {ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR} from '@angular/forms'; |
||||
|
import {Observable, of} from 'rxjs'; |
||||
|
import {PageLink} from '@shared/models/page/page-link'; |
||||
|
import {Direction} from '@shared/models/page/sort-order'; |
||||
|
import {map, mergeMap, startWith, tap} from 'rxjs/operators'; |
||||
|
import {PageData, emptyPageData} from '@shared/models/page/page-data'; |
||||
|
import {DashboardInfo} from '@app/shared/models/dashboard.models'; |
||||
|
import {DashboardId} from '@app/shared/models/id/dashboard-id'; |
||||
|
import {DashboardService} from '@core/http/dashboard.service'; |
||||
|
import {Store} from '@ngrx/store'; |
||||
|
import {AppState} from '@app/core/core.state'; |
||||
|
import {getCurrentAuthUser} from '@app/core/auth/auth.selectors'; |
||||
|
import {Authority} from '@shared/models/authority.enum'; |
||||
|
import {TranslateService} from '@ngx-translate/core'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-dashboard-autocomplete', |
||||
|
templateUrl: './dashboard-autocomplete.component.html', |
||||
|
styleUrls: [], |
||||
|
providers: [{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => DashboardAutocompleteComponent), |
||||
|
multi: true |
||||
|
}] |
||||
|
}) |
||||
|
export class DashboardAutocompleteComponent implements ControlValueAccessor, OnInit, AfterViewInit { |
||||
|
|
||||
|
selectDashboardFormGroup: FormGroup; |
||||
|
|
||||
|
modelValue: DashboardInfo | string | null; |
||||
|
|
||||
|
@Input() |
||||
|
useIdValue = true; |
||||
|
|
||||
|
@Input() |
||||
|
selectFirstDashboard = false; |
||||
|
|
||||
|
@Input() |
||||
|
placeholder: string; |
||||
|
|
||||
|
@Input() |
||||
|
dashboardsScope: 'customer' | 'tenant'; |
||||
|
|
||||
|
@Input() |
||||
|
tenantId: string; |
||||
|
|
||||
|
@Input() |
||||
|
customerId: string; |
||||
|
|
||||
|
@Input() |
||||
|
required: boolean; |
||||
|
|
||||
|
@Input() |
||||
|
disabled: boolean; |
||||
|
|
||||
|
@ViewChild('dashboardInput', {static: true}) dashboardInput: ElementRef; |
||||
|
|
||||
|
filteredDashboards: Observable<Array<DashboardInfo>>; |
||||
|
|
||||
|
private valueLoaded = false; |
||||
|
|
||||
|
private searchText = ''; |
||||
|
|
||||
|
private propagateChange = (v: any) => { }; |
||||
|
|
||||
|
constructor(private store: Store<AppState>, |
||||
|
public translate: TranslateService, |
||||
|
private dashboardService: DashboardService, |
||||
|
private fb: FormBuilder) { |
||||
|
this.selectDashboardFormGroup = this.fb.group({ |
||||
|
dashboard: [null] |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: any): void { |
||||
|
this.propagateChange = fn; |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(fn: any): void { |
||||
|
} |
||||
|
|
||||
|
ngOnInit() { |
||||
|
|
||||
|
} |
||||
|
|
||||
|
ngAfterViewInit(): void { |
||||
|
this.selectFirstDashboardIfNeeded(); |
||||
|
} |
||||
|
|
||||
|
selectFirstDashboardIfNeeded(): void { |
||||
|
if (this.selectFirstDashboard && !this.modelValue) { |
||||
|
this.getDashboards(new PageLink(1)).subscribe( |
||||
|
(data) => { |
||||
|
if (data.data.length) { |
||||
|
const dashboard = data.data[0]; |
||||
|
this.modelValue = this.useIdValue ? dashboard.id.id : dashboard; |
||||
|
this.selectDashboardFormGroup.get('dashboard').patchValue(dashboard, {emitEvent: false}); |
||||
|
this.propagateChange(this.modelValue); |
||||
|
} |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
setDisabledState(isDisabled: boolean): void { |
||||
|
this.disabled = isDisabled; |
||||
|
} |
||||
|
|
||||
|
initFilteredResults(): void { |
||||
|
this.filteredDashboards = this.selectDashboardFormGroup.get('dashboard').valueChanges |
||||
|
.pipe( |
||||
|
startWith<string | DashboardInfo>(''), |
||||
|
tap(value => { |
||||
|
if (this.valueLoaded) { |
||||
|
let modelValue; |
||||
|
if (typeof value === 'string' || !value) { |
||||
|
modelValue = null; |
||||
|
} else { |
||||
|
modelValue = this.useIdValue ? value.id.id : value; |
||||
|
} |
||||
|
this.updateView(modelValue); |
||||
|
} |
||||
|
}), |
||||
|
map(value => value ? (typeof value === 'string' ? value : value.name) : ''), |
||||
|
mergeMap(name => this.fetchDashboards(name) ) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
writeValue(value: DashboardInfo | string | null): void { |
||||
|
this.valueLoaded = false; |
||||
|
this.searchText = ''; |
||||
|
this.initFilteredResults(); |
||||
|
if (value != null) { |
||||
|
if (typeof value === 'string') { |
||||
|
this.dashboardService.getDashboardInfo(value).subscribe( |
||||
|
(dashboard) => { |
||||
|
this.modelValue = this.useIdValue ? dashboard.id.id : dashboard; |
||||
|
this.selectDashboardFormGroup.get('dashboard').patchValue(dashboard, {emitEvent: true}); |
||||
|
this.valueLoaded = true; |
||||
|
} |
||||
|
); |
||||
|
} else { |
||||
|
this.modelValue = this.useIdValue ? value.id.id : value; |
||||
|
this.selectDashboardFormGroup.get('dashboard').patchValue(value, {emitEvent: false}); |
||||
|
this.valueLoaded = true; |
||||
|
} |
||||
|
} else { |
||||
|
this.modelValue = null; |
||||
|
this.selectDashboardFormGroup.get('dashboard').patchValue(null, {emitEvent: false}); |
||||
|
this.valueLoaded = true; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
updateView(value: DashboardInfo | string | null) { |
||||
|
if (this.modelValue !== value) { |
||||
|
this.modelValue = value; |
||||
|
this.propagateChange(this.modelValue); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
displayDashboardFn(dashboard?: DashboardInfo): string | undefined { |
||||
|
return dashboard ? dashboard.title : undefined; |
||||
|
} |
||||
|
|
||||
|
fetchDashboards(searchText?: string): Observable<Array<DashboardInfo>> { |
||||
|
this.searchText = searchText; |
||||
|
const pageLink = new PageLink(10, 0, searchText, { |
||||
|
property: 'title', |
||||
|
direction: Direction.ASC |
||||
|
}); |
||||
|
return this.getDashboards(pageLink).pipe( |
||||
|
map(pageData => { |
||||
|
return pageData.data; |
||||
|
}) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
getDashboards(pageLink: PageLink): Observable<PageData<DashboardInfo>> { |
||||
|
let dashboardsObservable: Observable<PageData<DashboardInfo>>; |
||||
|
const authUser = getCurrentAuthUser(this.store); |
||||
|
if (this.dashboardsScope === 'customer' || authUser.authority === Authority.CUSTOMER_USER) { |
||||
|
if (this.customerId) { |
||||
|
dashboardsObservable = this.dashboardService.getCustomerDashboards(this.customerId, pageLink, false, true); |
||||
|
} else { |
||||
|
dashboardsObservable = of(emptyPageData()); |
||||
|
} |
||||
|
} else { |
||||
|
if (authUser.authority === Authority.SYS_ADMIN) { |
||||
|
if (this.tenantId) { |
||||
|
dashboardsObservable = this.dashboardService.getTenantDashboardsByTenantId(this.tenantId, pageLink, false, true); |
||||
|
} else { |
||||
|
dashboardsObservable = of(emptyPageData()); |
||||
|
} |
||||
|
} else { |
||||
|
dashboardsObservable = this.dashboardService.getTenantDashboards(pageLink, false, true); |
||||
|
} |
||||
|
} |
||||
|
return dashboardsObservable; |
||||
|
} |
||||
|
|
||||
|
clear() { |
||||
|
this.selectDashboardFormGroup.get('dashboard').patchValue(null, {emitEvent: true}); |
||||
|
setTimeout(() => { |
||||
|
this.dashboardInput.nativeElement.blur(); |
||||
|
this.dashboardInput.nativeElement.focus(); |
||||
|
}, 0); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,56 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2019 The Thingsboard Authors |
||||
|
|
||||
|
Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
you may not use this file except in compliance with the License. |
||||
|
You may obtain a copy of the License at |
||||
|
|
||||
|
http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
|
||||
|
Unless required by applicable law or agreed to in writing, software |
||||
|
distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
See the License for the specific language governing permissions and |
||||
|
limitations under the License. |
||||
|
|
||||
|
--> |
||||
|
<header> |
||||
|
<mat-toolbar color="primary" [ngStyle]="{height: headerHeightPx+'px'}"> |
||||
|
<div fxFlex fxLayout="row" fxLayoutAlign="start center"> |
||||
|
<div class="mat-toolbar-tools" fxFlex fxLayout="column" fxLayoutAlign="start start"> |
||||
|
<span class="tb-details-title">{{ headerTitle }}</span> |
||||
|
<span class="tb-details-subtitle">{{ headerSubtitle }}</span> |
||||
|
<span style="width: 100%;"> |
||||
|
<ng-content select=".header-pane"></ng-content> |
||||
|
</span> |
||||
|
</div> |
||||
|
<ng-content select=".details-buttons"></ng-content> |
||||
|
<button mat-button mat-icon-button (click)="onCloseDetails()"> |
||||
|
<mat-icon class="material-icons">close</mat-icon> |
||||
|
</button> |
||||
|
</div> |
||||
|
<section *ngIf="!isReadOnly" fxLayout="row" class="layout-wrap tb-header-buttons"> |
||||
|
<button [disabled]="(isLoading$ | async) || theForm.invalid || !theForm.dirty" |
||||
|
mat-fab |
||||
|
matTooltip="{{ 'action.apply-changes' | translate }}" |
||||
|
matTooltipPosition="above" |
||||
|
color="accent" class="tb-btn-header mat-fab-bottom-right" |
||||
|
[ngClass]="{'tb-hide': !isEdit}" |
||||
|
(click)="onApplyDetails()"> |
||||
|
<mat-icon class="material-icons">done</mat-icon> |
||||
|
</button> |
||||
|
<button [disabled]="(isLoading$ | async) || (isAlwaysEdit && !theForm.dirty)" |
||||
|
mat-fab |
||||
|
matTooltip="{{ (isAlwaysEdit ? 'action.decline-changes' : 'details.toggle-edit-mode') | translate }}" |
||||
|
matTooltipPosition="above" |
||||
|
color="accent" class="tb-btn-header mat-fab-bottom-right" |
||||
|
(click)="onToggleDetailsEditMode()"> |
||||
|
<mat-icon class="material-icons">{{isEdit ? 'close' : 'edit'}}</mat-icon> |
||||
|
</button> |
||||
|
</section> |
||||
|
</mat-toolbar> |
||||
|
</header> |
||||
|
<div fxFlex class="mat-content"> |
||||
|
<ng-content></ng-content> |
||||
|
</div> |
||||
@ -0,0 +1,53 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2019 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
@import '../../../scss/constants'; |
||||
|
|
||||
|
:host { |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
.mat-toolbar-tools { |
||||
|
height: 100%; |
||||
|
min-height: 100px; |
||||
|
max-height: 120px; |
||||
|
} |
||||
|
.tb-details-title { |
||||
|
width: inherit; |
||||
|
margin: 20px 8px 0 0; |
||||
|
overflow: hidden; |
||||
|
font-size: 1rem; |
||||
|
font-weight: 400; |
||||
|
text-overflow: ellipsis; |
||||
|
text-transform: uppercase; |
||||
|
white-space: nowrap; |
||||
|
|
||||
|
@media #{$mat-gt-sm} { |
||||
|
font-size: 1.6rem; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.tb-details-subtitle { |
||||
|
width: inherit; |
||||
|
margin: 10px 0; |
||||
|
overflow: hidden; |
||||
|
font-size: 1rem; |
||||
|
text-overflow: ellipsis; |
||||
|
white-space: nowrap; |
||||
|
opacity: .8; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,80 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2019 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { Component, EventEmitter, Input, Output } from '@angular/core'; |
||||
|
import { PageComponent } from '@shared/components/page.component'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { NgForm } from '@angular/forms'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-details-panel', |
||||
|
templateUrl: './details-panel.component.html', |
||||
|
styleUrls: ['./details-panel.component.scss'] |
||||
|
}) |
||||
|
export class DetailsPanelComponent extends PageComponent { |
||||
|
|
||||
|
@Input() headerHeightPx = 100; |
||||
|
@Input() headerTitle = ''; |
||||
|
@Input() headerSubtitle = ''; |
||||
|
@Input() isReadOnly = false; |
||||
|
@Input() isAlwaysEdit = false; |
||||
|
@Input() theForm: NgForm; |
||||
|
@Output() |
||||
|
closeDetails = new EventEmitter<void>(); |
||||
|
@Output() |
||||
|
toggleDetailsEditMode = new EventEmitter<boolean>(); |
||||
|
@Output() |
||||
|
applyDetails = new EventEmitter<void>(); |
||||
|
|
||||
|
isEditValue = false; |
||||
|
|
||||
|
@Output() |
||||
|
isEditChange = new EventEmitter<boolean>(); |
||||
|
|
||||
|
@Input() |
||||
|
get isEdit() { |
||||
|
return this.isEditValue; |
||||
|
} |
||||
|
|
||||
|
set isEdit(val: boolean) { |
||||
|
this.isEditValue = val; |
||||
|
this.isEditChange.emit(this.isEditValue); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
constructor(protected store: Store<AppState>) { |
||||
|
super(store); |
||||
|
} |
||||
|
|
||||
|
onCloseDetails() { |
||||
|
this.closeDetails.emit(); |
||||
|
} |
||||
|
|
||||
|
onToggleDetailsEditMode() { |
||||
|
if (!this.isAlwaysEdit) { |
||||
|
this.isEdit = !this.isEdit; |
||||
|
} |
||||
|
this.toggleDetailsEditMode.emit(this.isEditValue); |
||||
|
} |
||||
|
|
||||
|
onApplyDetails() { |
||||
|
if (this.theForm.valid) { |
||||
|
this.applyDetails.emit(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,51 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2019 The Thingsboard Authors |
||||
|
|
||||
|
Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
you may not use this file except in compliance with the License. |
||||
|
You may obtain a copy of the License at |
||||
|
|
||||
|
http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
|
||||
|
Unless required by applicable law or agreed to in writing, software |
||||
|
distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
See the License for the specific language governing permissions and |
||||
|
limitations under the License. |
||||
|
|
||||
|
--> |
||||
|
<form (ngSubmit)="add()" style="min-width: 400px;"> |
||||
|
<mat-toolbar fxLayout="row" color="primary"> |
||||
|
<h2 translate>{{ translations.add }}</h2> |
||||
|
<span fxFlex></span> |
||||
|
<div [tb-help]="resources.helpLinkId"></div> |
||||
|
<button mat-button mat-icon-button |
||||
|
(click)="cancel()" |
||||
|
type="button"> |
||||
|
<mat-icon class="material-icons">close</mat-icon> |
||||
|
</button> |
||||
|
</mat-toolbar> |
||||
|
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> |
||||
|
</mat-progress-bar> |
||||
|
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div> |
||||
|
<div mat-dialog-content> |
||||
|
<tb-anchor #entityDetailsForm></tb-anchor> |
||||
|
</div> |
||||
|
<div mat-dialog-actions fxLayout="row"> |
||||
|
<span fxFlex></span> |
||||
|
<button mat-button mat-raised-button color="primary" |
||||
|
type="submit" |
||||
|
[disabled]="(isLoading$ | async) || detailsForm.invalid || !detailsForm.dirty"> |
||||
|
{{ 'action.add' | translate }} |
||||
|
</button> |
||||
|
<button mat-button color="primary" |
||||
|
style="margin-right: 20px;" |
||||
|
type="button" |
||||
|
cdkFocusInitial |
||||
|
[disabled]="(isLoading$ | async)" |
||||
|
(click)="cancel()"> |
||||
|
{{ 'action.cancel' | translate }} |
||||
|
</button> |
||||
|
</div> |
||||
|
</form> |
||||
@ -0,0 +1,17 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2019 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
:host { |
||||
|
} |
||||
@ -0,0 +1,103 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2019 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { |
||||
|
Component, |
||||
|
ComponentFactoryResolver, |
||||
|
Inject, |
||||
|
OnInit, |
||||
|
SkipSelf, |
||||
|
ViewChild |
||||
|
} from '@angular/core'; |
||||
|
import { ErrorStateMatcher, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material'; |
||||
|
import { PageComponent } from '@shared/components/page.component'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { FormControl, FormGroupDirective, NgForm } from '@angular/forms'; |
||||
|
import { EntityTypeResource, EntityTypeTranslation } from '@shared/models/entity-type.models'; |
||||
|
import { EntityTableConfig } from '@shared/components/entity/entities-table-config.models'; |
||||
|
import { BaseData, HasId } from '@shared/models/base-data'; |
||||
|
import { EntityId } from '@shared/models/id/entity-id'; |
||||
|
import { AddEntityDialogData } from '@shared/components/entity/entity-component.models'; |
||||
|
import { TbAnchorComponent } from '@shared/components/tb-anchor.component'; |
||||
|
import { EntityComponent } from '@shared/components/entity/entity.component'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-add-entity-dialog', |
||||
|
templateUrl: './add-entity-dialog.component.html', |
||||
|
providers: [{provide: ErrorStateMatcher, useExisting: AddEntityDialogComponent}], |
||||
|
styleUrls: ['./add-entity-dialog.component.scss'] |
||||
|
}) |
||||
|
export class AddEntityDialogComponent extends PageComponent implements OnInit, ErrorStateMatcher { |
||||
|
|
||||
|
entityComponent: EntityComponent<BaseData<HasId>>; |
||||
|
detailsForm: NgForm; |
||||
|
|
||||
|
entitiesTableConfig: EntityTableConfig<BaseData<HasId>>; |
||||
|
translations: EntityTypeTranslation; |
||||
|
resources: EntityTypeResource; |
||||
|
entity: BaseData<EntityId>; |
||||
|
|
||||
|
submitted = false; |
||||
|
|
||||
|
@ViewChild('entityDetailsForm', {static: true}) entityDetailsFormAnchor: TbAnchorComponent; |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
@Inject(MAT_DIALOG_DATA) public data: AddEntityDialogData<BaseData<HasId>>, |
||||
|
public dialogRef: MatDialogRef<AddEntityDialogComponent, BaseData<HasId>>, |
||||
|
private componentFactoryResolver: ComponentFactoryResolver, |
||||
|
@SkipSelf() private errorStateMatcher: ErrorStateMatcher) { |
||||
|
super(store); |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
this.entitiesTableConfig = this.data.entitiesTableConfig; |
||||
|
this.translations = this.entitiesTableConfig.entityTranslations; |
||||
|
this.resources = this.entitiesTableConfig.entityResources; |
||||
|
this.entity = {}; |
||||
|
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.entitiesTableConfig.entityComponent); |
||||
|
const viewContainerRef = this.entityDetailsFormAnchor.viewContainerRef; |
||||
|
viewContainerRef.clear(); |
||||
|
const componentRef = viewContainerRef.createComponent(componentFactory); |
||||
|
this.entityComponent = componentRef.instance; |
||||
|
this.entityComponent.isEdit = true; |
||||
|
this.entityComponent.entitiesTableConfig = this.entitiesTableConfig; |
||||
|
this.entityComponent.entity = this.entity; |
||||
|
this.detailsForm = this.entityComponent.entityNgForm; |
||||
|
} |
||||
|
|
||||
|
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { |
||||
|
const originalErrorState = this.errorStateMatcher.isErrorState(control, form); |
||||
|
const customErrorState = !!(control && control.invalid && this.submitted); |
||||
|
return originalErrorState || customErrorState; |
||||
|
} |
||||
|
|
||||
|
cancel(): void { |
||||
|
this.dialogRef.close(null); |
||||
|
} |
||||
|
|
||||
|
add(): void { |
||||
|
this.submitted = true; |
||||
|
if (this.detailsForm.valid) { |
||||
|
this.entity = {...this.entity, ...this.entityComponent.entityFormValue()}; |
||||
|
this.entitiesTableConfig.saveEntity(this.entity).subscribe( |
||||
|
(entity) => { |
||||
|
this.dialogRef.close(entity); |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,84 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2019 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { EntityComponent } from '@shared/components/entity/entity.component'; |
||||
|
import { FormBuilder, FormGroup, ValidatorFn, Validators } from '@angular/forms'; |
||||
|
import { ContactBased } from '@shared/models/contact-based.model'; |
||||
|
import { AfterViewInit } from '@angular/core'; |
||||
|
import { POSTAL_CODE_PATTERNS } from '@shared/components/contact.models'; |
||||
|
import { HasId } from '@shared/models/base-data'; |
||||
|
|
||||
|
export abstract class ContactBasedComponent<T extends ContactBased<HasId>> extends EntityComponent<T> implements AfterViewInit { |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
protected fb: FormBuilder) { |
||||
|
super(store); |
||||
|
} |
||||
|
|
||||
|
buildForm(entity: T): FormGroup { |
||||
|
const entityForm = this.buildEntityForm(entity); |
||||
|
entityForm.addControl('country', this.fb.control(entity ? entity.country : '', [])); |
||||
|
entityForm.addControl('city', this.fb.control(entity ? entity.city : '', [])); |
||||
|
entityForm.addControl('state', this.fb.control(entity ? entity.state : '', [])); |
||||
|
entityForm.addControl('zip', this.fb.control(entity ? entity.zip : '', |
||||
|
this.zipValidators(entity ? entity.country : '') |
||||
|
)); |
||||
|
entityForm.addControl('address', this.fb.control(entity ? entity.address : '', [])); |
||||
|
entityForm.addControl('address2', this.fb.control(entity ? entity.address2 : '', [])); |
||||
|
entityForm.addControl('phone', this.fb.control(entity ? entity.phone : '', [])); |
||||
|
entityForm.addControl('email', this.fb.control(entity ? entity.email : '', [Validators.email])); |
||||
|
return entityForm; |
||||
|
} |
||||
|
|
||||
|
updateForm(entity: T) { |
||||
|
this.updateEntityForm(entity); |
||||
|
this.entityForm.patchValue({country: entity.country}); |
||||
|
this.entityForm.patchValue({city: entity.city}); |
||||
|
this.entityForm.patchValue({state: entity.state}); |
||||
|
this.entityForm.get('zip').setValidators(this.zipValidators(entity.country)); |
||||
|
this.entityForm.patchValue({zip: entity.zip}); |
||||
|
this.entityForm.patchValue({address: entity.address}); |
||||
|
this.entityForm.patchValue({address2: entity.address2}); |
||||
|
this.entityForm.patchValue({phone: entity.phone}); |
||||
|
this.entityForm.patchValue({email: entity.email}); |
||||
|
} |
||||
|
|
||||
|
ngAfterViewInit() { |
||||
|
this.entityForm.get('country').valueChanges.subscribe( |
||||
|
(country) => { |
||||
|
this.entityForm.get('zip').setValidators(this.zipValidators(country)); |
||||
|
this.entityForm.get('zip').updateValueAndValidity({onlySelf: true}); |
||||
|
this.entityForm.get('zip').markAsTouched({onlySelf: true}); |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
zipValidators(country: string): ValidatorFn[] { |
||||
|
const zipValidators = []; |
||||
|
if (country && POSTAL_CODE_PATTERNS[country]) { |
||||
|
const postalCodePattern = POSTAL_CODE_PATTERNS[country]; |
||||
|
zipValidators.push(Validators.pattern(postalCodePattern)); |
||||
|
} |
||||
|
return zipValidators; |
||||
|
} |
||||
|
|
||||
|
abstract buildEntityForm(entity: T): FormGroup; |
||||
|
|
||||
|
abstract updateEntityForm(entity: T); |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,137 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2019 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { BaseData, HasId } from '@shared/models/base-data'; |
||||
|
import { EntityId } from '@shared/models/id/entity-id'; |
||||
|
import { EntitiesFetchFunction } from '@shared/models/datasource/entity-datasource'; |
||||
|
import { Observable, of } from 'rxjs'; |
||||
|
import { emptyPageData } from '@shared/models/page/page-data'; |
||||
|
import { DatePipe } from '@angular/common'; |
||||
|
import { Direction, SortOrder } from '@shared/models/page/sort-order'; |
||||
|
import { |
||||
|
EntityType, |
||||
|
EntityTypeResource, |
||||
|
EntityTypeTranslation |
||||
|
} from '@shared/models/entity-type.models'; |
||||
|
import { EntityComponent } from '@shared/components/entity/entity.component'; |
||||
|
import { Type } from '@angular/core'; |
||||
|
import { EntityAction } from '@shared/components/entity/entity-component.models'; |
||||
|
import { HasUUID } from '@shared/models/id/has-uuid'; |
||||
|
import { PageLink } from '@shared/models/page/page-link'; |
||||
|
import { EntitiesTableComponent } from '@shared/components/entity/entities-table.component'; |
||||
|
import { EntityTableHeaderComponent } from '@shared/components/entity/entity-table-header.component'; |
||||
|
import { ActivatedRoute } from '@angular/router'; |
||||
|
|
||||
|
export type EntityBooleanFunction<T extends BaseData<HasId>> = (entity: T) => boolean; |
||||
|
export type EntityStringFunction<T extends BaseData<HasId>> = (entity: T) => string; |
||||
|
export type EntityCountStringFunction = (count: number) => string; |
||||
|
export type EntityTwoWayOperation<T extends BaseData<HasId>> = (entity: T) => Observable<T>; |
||||
|
export type EntityByIdOperation<T extends BaseData<HasId>> = (id: HasUUID) => Observable<T>; |
||||
|
export type EntityIdOneWayOperation = (id: HasUUID) => Observable<any>; |
||||
|
export type EntityActionFunction<T extends BaseData<HasId>> = (action: EntityAction<T>) => boolean; |
||||
|
export type CreateEntityOperation<T extends BaseData<HasId>> = () => Observable<T>; |
||||
|
|
||||
|
export type CellContentFunction<T extends BaseData<HasId>> = (entity: T, key: string) => string; |
||||
|
export type CellStyleFunction<T extends BaseData<HasId>> = (entity: T, key: string) => object; |
||||
|
|
||||
|
export interface CellActionDescriptor<T extends BaseData<HasId>> { |
||||
|
name: string; |
||||
|
nameFunction?: (entity: T) => string; |
||||
|
icon?: string; |
||||
|
isMdiIcon?: boolean; |
||||
|
color?: string; |
||||
|
isEnabled: (entity: T) => boolean; |
||||
|
onAction: ($event: MouseEvent, entity: T) => void; |
||||
|
} |
||||
|
|
||||
|
export interface GroupActionDescriptor<T extends BaseData<HasId>> { |
||||
|
name: string; |
||||
|
icon: string; |
||||
|
isEnabled: boolean; |
||||
|
onAction: ($event: MouseEvent, entities: T[]) => void; |
||||
|
} |
||||
|
|
||||
|
export interface HeaderActionDescriptor { |
||||
|
name: string; |
||||
|
icon: string; |
||||
|
isEnabled: () => boolean; |
||||
|
onAction: ($event: MouseEvent) => void; |
||||
|
} |
||||
|
|
||||
|
export class EntityTableColumn<T extends BaseData<HasId>> { |
||||
|
constructor(public key: string, |
||||
|
public title: string, |
||||
|
public maxWidth: string = '100%', |
||||
|
public cellContentFunction: CellContentFunction<T> = (entity, property) => entity[property], |
||||
|
public cellStyleFunction: CellStyleFunction<T> = () => ({})) { |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export class DateEntityTableColumn<T extends BaseData<HasId>> extends EntityTableColumn<T> { |
||||
|
constructor(key: string, |
||||
|
title: string, |
||||
|
datePipe: DatePipe, |
||||
|
maxWidth: string = '100%', |
||||
|
dateFormat: string = 'yyyy-MM-dd HH:mm:ss', |
||||
|
cellStyleFunction: CellStyleFunction<T> = () => ({})) { |
||||
|
super(key, |
||||
|
title, |
||||
|
maxWidth, |
||||
|
(entity, property) => datePipe.transform(entity[property], dateFormat), |
||||
|
cellStyleFunction); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export class EntityTableConfig<T extends BaseData<HasId>, P extends PageLink = PageLink> { |
||||
|
|
||||
|
constructor() {} |
||||
|
|
||||
|
componentsData: any = null; |
||||
|
|
||||
|
loadDataOnInit = true; |
||||
|
onLoadAction: (route: ActivatedRoute) => void = null; |
||||
|
table: EntitiesTableComponent = null; |
||||
|
useTimePageLink = false; |
||||
|
entityType: EntityType = null; |
||||
|
tableTitle = ''; |
||||
|
selectionEnabled = true; |
||||
|
searchEnabled = true; |
||||
|
addEnabled = true; |
||||
|
entitiesDeleteEnabled = true; |
||||
|
detailsPanelEnabled = true; |
||||
|
actionsColumnTitle = null; |
||||
|
entityTranslations: EntityTypeTranslation; |
||||
|
entityResources: EntityTypeResource; |
||||
|
entityComponent: Type<EntityComponent<T>>; |
||||
|
defaultSortOrder: SortOrder = {property: 'createdTime', direction: Direction.ASC}; |
||||
|
columns: Array<EntityTableColumn<T>> = []; |
||||
|
cellActionDescriptors: Array<CellActionDescriptor<T>> = []; |
||||
|
groupActionDescriptors: Array<GroupActionDescriptor<T>> = []; |
||||
|
headerActionDescriptors: Array<HeaderActionDescriptor> = []; |
||||
|
headerComponent: Type<EntityTableHeaderComponent<T>>; |
||||
|
addEntity: CreateEntityOperation<T> = null; |
||||
|
detailsReadonly: EntityBooleanFunction<T> = () => false; |
||||
|
deleteEnabled: EntityBooleanFunction<T> = () => true; |
||||
|
deleteEntityTitle: EntityStringFunction<T> = () => ''; |
||||
|
deleteEntityContent: EntityStringFunction<T> = () => ''; |
||||
|
deleteEntitiesTitle: EntityCountStringFunction = () => ''; |
||||
|
deleteEntitiesContent: EntityCountStringFunction = () => ''; |
||||
|
loadEntity: EntityByIdOperation<T> = () => of(); |
||||
|
saveEntity: EntityTwoWayOperation<T> = (entity) => of(entity); |
||||
|
deleteEntity: EntityIdOneWayOperation = () => of(); |
||||
|
entitiesFetchFunction: EntitiesFetchFunction<T, P> = () => of(emptyPageData<T>()); |
||||
|
onEntityAction: EntityActionFunction<T> = () => false; |
||||
|
} |
||||
@ -0,0 +1,182 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2019 The Thingsboard Authors |
||||
|
|
||||
|
Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
you may not use this file except in compliance with the License. |
||||
|
You may obtain a copy of the License at |
||||
|
|
||||
|
http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
|
||||
|
Unless required by applicable law or agreed to in writing, software |
||||
|
distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
See the License for the specific language governing permissions and |
||||
|
limitations under the License. |
||||
|
|
||||
|
--> |
||||
|
<mat-drawer-container hasBackdrop="false" class="tb-absolute-fill"> |
||||
|
<mat-drawer *ngIf="entitiesTableConfig.detailsPanelEnabled" |
||||
|
class="tb-details-drawer mat-elevation-z4" |
||||
|
#drawer |
||||
|
mode="over" |
||||
|
position="end" |
||||
|
[opened]="isDetailsOpen"> |
||||
|
<tb-entity-details-panel |
||||
|
[entitiesTableConfig]="entitiesTableConfig" |
||||
|
[entityId]="dataSource.currentEntity?.id" |
||||
|
(closeEntityDetails)="isDetailsOpen = false" |
||||
|
(entityUpdated)="onEntityUpdated($event)" |
||||
|
(entityAction)="onEntityAction($event)" |
||||
|
> |
||||
|
</tb-entity-details-panel> |
||||
|
</mat-drawer> |
||||
|
<mat-drawer-content> |
||||
|
<div class="mat-padding tb-entity-table tb-absolute-fill"> |
||||
|
<div fxFlex fxLayout="column" class="mat-elevation-z1 tb-entity-table-content"> |
||||
|
<mat-toolbar class="mat-table-toolbar" [fxShow]="!textSearchMode && dataSource.selection.isEmpty()"> |
||||
|
<div class="mat-toolbar-tools"> |
||||
|
<span *ngIf="entitiesTableConfig.tableTitle" class="tb-entity-table-title">{{ entitiesTableConfig.tableTitle }}</span> |
||||
|
<tb-timewindow *ngIf="entitiesTableConfig.useTimePageLink" [(ngModel)]="timewindow" |
||||
|
(ngModelChange)="onTimewindowChange()" |
||||
|
asButton historyOnly></tb-timewindow> |
||||
|
<tb-anchor #entityTableHeader></tb-anchor> |
||||
|
<span fxFlex *ngIf="!this.entitiesTableConfig.headerComponent"></span> |
||||
|
<button mat-button mat-icon-button [disabled]="isLoading$ | async" [fxShow]="addEnabled()" (click)="addEntity($event)" |
||||
|
matTooltip="{{ translations.add | translate }}" |
||||
|
matTooltipPosition="above"> |
||||
|
<mat-icon>add</mat-icon> |
||||
|
</button> |
||||
|
<button mat-button mat-icon-button [disabled]="isLoading$ | async" |
||||
|
[fxShow]="actionDescriptor.isEnabled()" *ngFor="let actionDescriptor of headerActionDescriptors" |
||||
|
matTooltip="{{ actionDescriptor.name }}" |
||||
|
matTooltipPosition="above" |
||||
|
(click)="actionDescriptor.onAction($event)"> |
||||
|
<mat-icon>{{actionDescriptor.icon}}</mat-icon> |
||||
|
</button> |
||||
|
<button mat-button mat-icon-button [disabled]="isLoading$ | async" (click)="updateData()" |
||||
|
matTooltip="{{ 'action.refresh' | translate }}" |
||||
|
matTooltipPosition="above"> |
||||
|
<mat-icon>refresh</mat-icon> |
||||
|
</button> |
||||
|
<button *ngIf="entitiesTableConfig.searchEnabled" |
||||
|
mat-button mat-icon-button [disabled]="isLoading$ | async" (click)="enterFilterMode()" |
||||
|
matTooltip="{{ translations.search | translate }}" |
||||
|
matTooltipPosition="above"> |
||||
|
<mat-icon>search</mat-icon> |
||||
|
</button> |
||||
|
</div> |
||||
|
</mat-toolbar> |
||||
|
<mat-toolbar class="mat-table-toolbar" [fxShow]="textSearchMode && dataSource.selection.isEmpty()"> |
||||
|
<div class="mat-toolbar-tools"> |
||||
|
<button mat-button mat-icon-button |
||||
|
matTooltip="{{ translations.search | translate }}" |
||||
|
matTooltipPosition="above"> |
||||
|
<mat-icon>search</mat-icon> |
||||
|
</button> |
||||
|
<mat-form-field fxFlex> |
||||
|
<mat-label> </mat-label> |
||||
|
<input #searchInput matInput |
||||
|
[(ngModel)]="pageLink.textSearch" |
||||
|
placeholder="{{ translations.search | translate }}"/> |
||||
|
</mat-form-field> |
||||
|
<button mat-button mat-icon-button (click)="exitFilterMode()" |
||||
|
matTooltip="{{ 'action.close' | translate }}" |
||||
|
matTooltipPosition="above"> |
||||
|
<mat-icon>close</mat-icon> |
||||
|
</button> |
||||
|
</div> |
||||
|
</mat-toolbar> |
||||
|
<mat-toolbar *ngIf="entitiesTableConfig.selectionEnabled" class="mat-table-toolbar" color="primary" [fxShow]="!dataSource.selection.isEmpty()"> |
||||
|
<div class="mat-toolbar-tools"> |
||||
|
<span> |
||||
|
{{ translate.get(translations.selectedEntities, {count: dataSource.selection.selected.length}) | async }} |
||||
|
</span> |
||||
|
<span fxFlex></span> |
||||
|
<button mat-button mat-icon-button [disabled]="isLoading$ | async" |
||||
|
[fxShow]="actionDescriptor.isEnabled" *ngFor="let actionDescriptor of groupActionDescriptors" |
||||
|
matTooltip="{{ actionDescriptor.name }}" |
||||
|
matTooltipPosition="above" |
||||
|
(click)="actionDescriptor.onAction($event, dataSource.selection.selected)"> |
||||
|
<mat-icon>{{actionDescriptor.icon}}</mat-icon> |
||||
|
</button> |
||||
|
</div> |
||||
|
</mat-toolbar> |
||||
|
<div fxFlex class="table-container"> |
||||
|
<mat-table [dataSource]="dataSource" |
||||
|
matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="(pageLink.sortOrder.direction + '').toLowerCase()" matSortDisableClear> |
||||
|
<ng-container matColumnDef="select" sticky> |
||||
|
<mat-header-cell *matHeaderCellDef> |
||||
|
<mat-checkbox (change)="$event ? dataSource.masterToggle() : null" |
||||
|
[checked]="dataSource.selection.hasValue() && (dataSource.isAllSelected() | async)" |
||||
|
[indeterminate]="dataSource.selection.hasValue() && !(dataSource.isAllSelected() | async)"> |
||||
|
</mat-checkbox> |
||||
|
</mat-header-cell> |
||||
|
<mat-cell *matCellDef="let entity"> |
||||
|
<mat-checkbox (click)="$event.stopPropagation()" |
||||
|
(change)="$event ? dataSource.selection.toggle(entity) : null" |
||||
|
[checked]="dataSource.selection.isSelected(entity)"> |
||||
|
</mat-checkbox> |
||||
|
</mat-cell> |
||||
|
</ng-container> |
||||
|
<ng-container [matColumnDef]="column.key" *ngFor="let column of columns"> |
||||
|
<mat-header-cell *matHeaderCellDef [ngStyle]="{maxWidth: column.maxWidth}" mat-sort-header> {{ column.title | translate }} </mat-header-cell> |
||||
|
<mat-cell *matCellDef="let entity" [ngStyle]="cellStyle(entity, column)" [innerHTML]="cellContent(entity, column)"></mat-cell> |
||||
|
</ng-container> |
||||
|
<ng-container matColumnDef="actions" stickyEnd> |
||||
|
<mat-header-cell *matHeaderCellDef [ngStyle.gt-md]="{ minWidth: (cellActionDescriptors.length * 40) + 'px' }"> |
||||
|
{{ entitiesTableConfig.actionsColumnTitle ? (entitiesTableConfig.actionsColumnTitle | translate) : '' }} |
||||
|
</mat-header-cell> |
||||
|
<mat-cell *matCellDef="let entity" [ngStyle.gt-md]="{ minWidth: (cellActionDescriptors.length * 40) + 'px' }"> |
||||
|
<div fxHide fxShow.gt-md fxFlex fxLayout="row" fxLayoutAlign="end"> |
||||
|
<button mat-button mat-icon-button [disabled]="isLoading$ | async" |
||||
|
[fxShow]="actionDescriptor.isEnabled(entity)" *ngFor="let actionDescriptor of cellActionDescriptors" |
||||
|
matTooltip="{{ actionDescriptor.nameFunction ? actionDescriptor.nameFunction(entity) : actionDescriptor.name }}" |
||||
|
matTooltipPosition="above" |
||||
|
(click)="actionDescriptor.onAction($event, entity)"> |
||||
|
<mat-icon *ngIf="!actionDescriptor.isMdiIcon" [ngStyle]="actionDescriptor.color ? {color: actionDescriptor.color} : {}"> |
||||
|
{{actionDescriptor.icon}}</mat-icon> |
||||
|
<mat-icon *ngIf="actionDescriptor.isMdiIcon" [ngStyle]="actionDescriptor.color ? {color: actionDescriptor.color} : {}" |
||||
|
[svgIcon]="actionDescriptor.icon"></mat-icon> |
||||
|
</button> |
||||
|
</div> |
||||
|
<div fxHide fxShow.lt-lg> |
||||
|
<button mat-button mat-icon-button |
||||
|
(click)="$event.stopPropagation()" |
||||
|
[matMenuTriggerFor]="cellActionsMenu"> |
||||
|
<mat-icon class="material-icons">more_vert</mat-icon> |
||||
|
</button> |
||||
|
<mat-menu #cellActionsMenu="matMenu" xPosition="before"> |
||||
|
<button mat-menu-item *ngFor="let actionDescriptor of cellActionDescriptors" |
||||
|
[disabled]="isLoading$ | async" |
||||
|
[fxShow]="actionDescriptor.isEnabled(entity)" |
||||
|
(click)="actionDescriptor.onAction($event, entity)"> |
||||
|
<mat-icon *ngIf="!actionDescriptor.isMdiIcon" [ngStyle]="actionDescriptor.color ? {color: actionDescriptor.color} : {}"> |
||||
|
{{actionDescriptor.icon}}</mat-icon> |
||||
|
<mat-icon *ngIf="actionDescriptor.isMdiIcon" [ngStyle]="actionDescriptor.color ? {color: actionDescriptor.color} : {}" |
||||
|
[svgIcon]="actionDescriptor.icon"></mat-icon> |
||||
|
<span>{{ actionDescriptor.nameFunction ? actionDescriptor.nameFunction(entity) : actionDescriptor.name }}</span> |
||||
|
</button> |
||||
|
</mat-menu> |
||||
|
</div> |
||||
|
</mat-cell> |
||||
|
</ng-container> |
||||
|
<mat-header-row [ngClass]="{'mat-row-select': selectionEnabled}" *matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row> |
||||
|
<mat-row [ngClass]="{'mat-row-select': selectionEnabled, |
||||
|
'mat-selected': dataSource.selection.isSelected(entity), |
||||
|
'tb-current-entity': dataSource.isCurrentEntity(entity)}" |
||||
|
*matRowDef="let entity; columns: displayedColumns;" (click)="onRowClick($event, entity)"></mat-row> |
||||
|
</mat-table> |
||||
|
<span [fxShow]="dataSource.isEmpty() | async" |
||||
|
fxLayoutAlign="center center" |
||||
|
class="no-data-found" translate>{{ translations.noEntities }}</span> |
||||
|
</div> |
||||
|
<mat-divider></mat-divider> |
||||
|
<mat-paginator [length]="dataSource.total() | async" |
||||
|
[pageIndex]="pageLink.page" |
||||
|
[pageSize]="pageLink.pageSize" |
||||
|
[pageSizeOptions]="[10, 20, 30]"></mat-paginator> |
||||
|
</div> |
||||
|
</div> |
||||
|
</mat-drawer-content> |
||||
|
</mat-drawer-container> |
||||
@ -0,0 +1,41 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2019 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
:host { |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
.tb-entity-table { |
||||
|
.tb-entity-table-content { |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
background: #fff; |
||||
|
|
||||
|
.tb-entity-table-title { |
||||
|
padding-right: 20px; |
||||
|
white-space: nowrap; |
||||
|
overflow: hidden; |
||||
|
text-overflow: ellipsis; |
||||
|
} |
||||
|
|
||||
|
.table-container { |
||||
|
overflow: auto; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
:host ::ng-deep .mat-sort-header-sorted .mat-sort-header-arrow { |
||||
|
opacity: 1 !important; |
||||
|
} |
||||
@ -0,0 +1,358 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2019 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { |
||||
|
AfterViewInit, |
||||
|
Component, ComponentFactoryResolver, |
||||
|
ElementRef, |
||||
|
Input, |
||||
|
OnInit, |
||||
|
Type, |
||||
|
ViewChild |
||||
|
} from '@angular/core'; |
||||
|
import { PageComponent } from '@shared/components/page.component'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { PageLink, TimePageLink } from '@shared/models/page/page-link'; |
||||
|
import { MatDialog, MatPaginator, MatSort } from '@angular/material'; |
||||
|
import { EntitiesDataSource } from '@shared/models/datasource/entity-datasource'; |
||||
|
import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators'; |
||||
|
import { Direction, SortOrder } from '@shared/models/page/sort-order'; |
||||
|
import { forkJoin, fromEvent, merge, Observable } from 'rxjs'; |
||||
|
import { TranslateService } from '@ngx-translate/core'; |
||||
|
import { BaseData, HasId } from '@shared/models/base-data'; |
||||
|
import { EntityId } from '@shared/models/id/entity-id'; |
||||
|
import { ActivatedRoute } from '@angular/router'; |
||||
|
import { |
||||
|
CellActionDescriptor, |
||||
|
EntityTableColumn, |
||||
|
EntityTableConfig, |
||||
|
GroupActionDescriptor, |
||||
|
HeaderActionDescriptor |
||||
|
} from '@shared/components/entity/entities-table-config.models'; |
||||
|
import { EntityTypeTranslation } from '@shared/models/entity-type.models'; |
||||
|
import { DialogService } from '@core/services/dialog.service'; |
||||
|
import { AddEntityDialogComponent } from '@shared/components/entity/add-entity-dialog.component'; |
||||
|
import { |
||||
|
AddEntityDialogData, |
||||
|
EntityAction |
||||
|
} from '@shared/components/entity/entity-component.models'; |
||||
|
import { Timewindow } from '@shared/models/time/time.models'; |
||||
|
import { DomSanitizer } from '@angular/platform-browser'; |
||||
|
import { TbAnchorComponent } from '@shared/components/tb-anchor.component'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-entities-table', |
||||
|
templateUrl: './entities-table.component.html', |
||||
|
styleUrls: ['./entities-table.component.scss'] |
||||
|
}) |
||||
|
export class EntitiesTableComponent extends PageComponent implements AfterViewInit, OnInit { |
||||
|
|
||||
|
@Input() |
||||
|
entitiesTableConfig: EntityTableConfig<BaseData<HasId>>; |
||||
|
|
||||
|
translations: EntityTypeTranslation; |
||||
|
|
||||
|
headerActionDescriptors: Array<HeaderActionDescriptor>; |
||||
|
groupActionDescriptors: Array<GroupActionDescriptor<BaseData<HasId>>>; |
||||
|
cellActionDescriptors: Array<CellActionDescriptor<BaseData<HasId>>>; |
||||
|
|
||||
|
columns: Array<EntityTableColumn<BaseData<HasId>>>; |
||||
|
displayedColumns: string[] = []; |
||||
|
|
||||
|
selectionEnabled; |
||||
|
|
||||
|
pageLink: PageLink; |
||||
|
textSearchMode = false; |
||||
|
timewindow: Timewindow; |
||||
|
dataSource: EntitiesDataSource<BaseData<HasId>>; |
||||
|
|
||||
|
isDetailsOpen = false; |
||||
|
|
||||
|
@ViewChild('entityTableHeader', {static: false}) entityTableHeaderAnchor: TbAnchorComponent; |
||||
|
|
||||
|
@ViewChild('searchInput', {static: false}) searchInputField: ElementRef; |
||||
|
|
||||
|
@ViewChild(MatPaginator, {static: false}) paginator: MatPaginator; |
||||
|
@ViewChild(MatSort, {static: false}) sort: MatSort; |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
private route: ActivatedRoute, |
||||
|
public translate: TranslateService, |
||||
|
public dialog: MatDialog, |
||||
|
private dialogService: DialogService, |
||||
|
private domSanitizer: DomSanitizer, |
||||
|
private componentFactoryResolver: ComponentFactoryResolver) { |
||||
|
super(store); |
||||
|
} |
||||
|
|
||||
|
ngOnInit() { |
||||
|
this.entitiesTableConfig = this.entitiesTableConfig || this.route.snapshot.data.entitiesTableConfig; |
||||
|
if (this.entitiesTableConfig.headerComponent) { |
||||
|
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.entitiesTableConfig.headerComponent); |
||||
|
const viewContainerRef = this.entityTableHeaderAnchor.viewContainerRef; |
||||
|
viewContainerRef.clear(); |
||||
|
const componentRef = viewContainerRef.createComponent(componentFactory); |
||||
|
const headerComponent = componentRef.instance; |
||||
|
headerComponent.entitiesTableConfig = this.entitiesTableConfig; |
||||
|
} |
||||
|
|
||||
|
this.entitiesTableConfig.table = this; |
||||
|
this.translations = this.entitiesTableConfig.entityTranslations; |
||||
|
|
||||
|
this.headerActionDescriptors = [...this.entitiesTableConfig.headerActionDescriptors]; |
||||
|
this.groupActionDescriptors = [...this.entitiesTableConfig.groupActionDescriptors]; |
||||
|
this.cellActionDescriptors = [...this.entitiesTableConfig.cellActionDescriptors]; |
||||
|
|
||||
|
if (this.entitiesTableConfig.entitiesDeleteEnabled) { |
||||
|
this.cellActionDescriptors.push( |
||||
|
{ |
||||
|
name: this.translate.instant('action.delete'), |
||||
|
icon: 'delete', |
||||
|
isEnabled: entity => this.entitiesTableConfig.deleteEnabled(entity), |
||||
|
onAction: ($event, entity) => this.deleteEntity($event, entity) |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
this.groupActionDescriptors.push( |
||||
|
{ |
||||
|
name: this.translate.instant('action.delete'), |
||||
|
icon: 'delete', |
||||
|
isEnabled: this.entitiesTableConfig.entitiesDeleteEnabled, |
||||
|
onAction: ($event, entities) => this.deleteEntities($event, entities) |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
this.columns = [...this.entitiesTableConfig.columns]; |
||||
|
|
||||
|
this.selectionEnabled = this.entitiesTableConfig.selectionEnabled; |
||||
|
|
||||
|
if (this.selectionEnabled) { |
||||
|
this.displayedColumns.push('select'); |
||||
|
} |
||||
|
this.columns.forEach( |
||||
|
(column) => { |
||||
|
this.displayedColumns.push(column.key); |
||||
|
} |
||||
|
); |
||||
|
this.displayedColumns.push('actions'); |
||||
|
|
||||
|
const sortOrder: SortOrder = { property: this.entitiesTableConfig.defaultSortOrder.property, |
||||
|
direction: this.entitiesTableConfig.defaultSortOrder.direction }; |
||||
|
|
||||
|
if (this.entitiesTableConfig.useTimePageLink) { |
||||
|
this.timewindow = Timewindow.historyInterval(24 * 60 * 60 * 1000); |
||||
|
const currentTime = new Date().getTime(); |
||||
|
this.pageLink = new TimePageLink(10, 0, null, sortOrder, |
||||
|
currentTime - this.timewindow.history.timewindowMs, currentTime); |
||||
|
} else { |
||||
|
this.pageLink = new PageLink(10, 0, null, sortOrder); |
||||
|
} |
||||
|
this.dataSource = new EntitiesDataSource<BaseData<HasId>>( |
||||
|
this.entitiesTableConfig.entitiesFetchFunction |
||||
|
); |
||||
|
if (this.entitiesTableConfig.onLoadAction) { |
||||
|
this.entitiesTableConfig.onLoadAction(this.route); |
||||
|
} |
||||
|
if (this.entitiesTableConfig.loadDataOnInit) { |
||||
|
this.dataSource.loadEntities(this.pageLink); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
ngAfterViewInit() { |
||||
|
|
||||
|
fromEvent(this.searchInputField.nativeElement, 'keyup') |
||||
|
.pipe( |
||||
|
debounceTime(150), |
||||
|
distinctUntilChanged(), |
||||
|
tap(() => { |
||||
|
this.paginator.pageIndex = 0; |
||||
|
this.updateData(); |
||||
|
}) |
||||
|
) |
||||
|
.subscribe(); |
||||
|
|
||||
|
this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0); |
||||
|
|
||||
|
merge(this.sort.sortChange, this.paginator.page) |
||||
|
.pipe( |
||||
|
tap(() => this.updateData()) |
||||
|
) |
||||
|
.subscribe(); |
||||
|
} |
||||
|
|
||||
|
addEnabled() { |
||||
|
return this.entitiesTableConfig.addEnabled; |
||||
|
} |
||||
|
|
||||
|
updateData(closeDetails: boolean = true) { |
||||
|
if (closeDetails) { |
||||
|
this.isDetailsOpen = false; |
||||
|
} |
||||
|
this.pageLink.page = this.paginator.pageIndex; |
||||
|
this.pageLink.pageSize = this.paginator.pageSize; |
||||
|
this.pageLink.sortOrder.property = this.sort.active; |
||||
|
this.pageLink.sortOrder.direction = Direction[this.sort.direction.toUpperCase()]; |
||||
|
if (this.entitiesTableConfig.useTimePageLink) { |
||||
|
const timePageLink = this.pageLink as TimePageLink; |
||||
|
if (this.timewindow.history.timewindowMs) { |
||||
|
const currentTime = new Date().getTime(); |
||||
|
timePageLink.startTime = currentTime - this.timewindow.history.timewindowMs; |
||||
|
timePageLink.endTime = currentTime; |
||||
|
} else { |
||||
|
timePageLink.startTime = this.timewindow.history.fixedTimewindow.startTimeMs; |
||||
|
timePageLink.endTime = this.timewindow.history.fixedTimewindow.endTimeMs; |
||||
|
} |
||||
|
} |
||||
|
this.dataSource.loadEntities(this.pageLink); |
||||
|
} |
||||
|
|
||||
|
onRowClick($event: Event, entity) { |
||||
|
if ($event) { |
||||
|
$event.stopPropagation(); |
||||
|
} |
||||
|
if (this.dataSource.toggleCurrentEntity(entity)) { |
||||
|
this.isDetailsOpen = true; |
||||
|
} else { |
||||
|
this.isDetailsOpen = !this.isDetailsOpen; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
addEntity($event: Event) { |
||||
|
let entity$: Observable<BaseData<HasId>>; |
||||
|
if (this.entitiesTableConfig.addEntity) { |
||||
|
entity$ = this.entitiesTableConfig.addEntity(); |
||||
|
} else { |
||||
|
entity$ = this.dialog.open<AddEntityDialogComponent, AddEntityDialogData<BaseData<HasId>>, |
||||
|
BaseData<HasId>>(AddEntityDialogComponent, { |
||||
|
disableClose: true, |
||||
|
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], |
||||
|
data: { |
||||
|
entitiesTableConfig: this.entitiesTableConfig |
||||
|
} |
||||
|
}).afterClosed(); |
||||
|
} |
||||
|
entity$.subscribe( |
||||
|
(entity) => { |
||||
|
if (entity) { |
||||
|
this.updateData(); |
||||
|
} |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
onEntityUpdated(entity: BaseData<HasId>) { |
||||
|
this.updateData(false); |
||||
|
} |
||||
|
|
||||
|
onEntityAction(action: EntityAction<BaseData<HasId>>) { |
||||
|
if (action.action === 'delete') { |
||||
|
this.deleteEntity(action.event, action.entity); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
deleteEntity($event: Event, entity: BaseData<HasId>) { |
||||
|
if ($event) { |
||||
|
$event.stopPropagation(); |
||||
|
} |
||||
|
this.dialogService.confirm( |
||||
|
this.entitiesTableConfig.deleteEntityTitle(entity), |
||||
|
this.entitiesTableConfig.deleteEntityContent(entity), |
||||
|
this.translate.instant('action.no'), |
||||
|
this.translate.instant('action.yes'), |
||||
|
true |
||||
|
).subscribe((result) => { |
||||
|
if (result) { |
||||
|
this.entitiesTableConfig.deleteEntity(entity.id).subscribe( |
||||
|
() => { |
||||
|
this.updateData(); |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
deleteEntities($event: Event, entities: BaseData<HasId>[]) { |
||||
|
if ($event) { |
||||
|
$event.stopPropagation(); |
||||
|
} |
||||
|
this.dialogService.confirm( |
||||
|
this.entitiesTableConfig.deleteEntitiesTitle(entities.length), |
||||
|
this.entitiesTableConfig.deleteEntitiesContent(entities.length), |
||||
|
this.translate.instant('action.no'), |
||||
|
this.translate.instant('action.yes'), |
||||
|
true |
||||
|
).subscribe((result) => { |
||||
|
if (result) { |
||||
|
const tasks: Observable<any>[] = []; |
||||
|
entities.forEach((entity) => { |
||||
|
if (this.entitiesTableConfig.deleteEnabled(entity)) { |
||||
|
tasks.push(this.entitiesTableConfig.deleteEntity(entity.id)); |
||||
|
} |
||||
|
}); |
||||
|
forkJoin(tasks).subscribe( |
||||
|
() => { |
||||
|
this.updateData(); |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
onTimewindowChange() { |
||||
|
this.updateData(); |
||||
|
} |
||||
|
|
||||
|
enterFilterMode() { |
||||
|
this.textSearchMode = true; |
||||
|
this.pageLink.textSearch = ''; |
||||
|
setTimeout(() => { |
||||
|
this.searchInputField.nativeElement.focus(); |
||||
|
this.searchInputField.nativeElement.setSelectionRange(0, 0); |
||||
|
}, 10); |
||||
|
} |
||||
|
|
||||
|
exitFilterMode() { |
||||
|
this.textSearchMode = false; |
||||
|
this.pageLink.textSearch = null; |
||||
|
this.paginator.pageIndex = 0; |
||||
|
this.updateData(); |
||||
|
} |
||||
|
|
||||
|
resetSortAndFilter(update: boolean = true) { |
||||
|
this.pageLink.textSearch = null; |
||||
|
if (this.entitiesTableConfig.useTimePageLink) { |
||||
|
this.timewindow = Timewindow.historyInterval(24 * 60 * 60 * 1000); |
||||
|
} |
||||
|
this.paginator.pageIndex = 0; |
||||
|
const sortable = this.sort.sortables.get(this.entitiesTableConfig.defaultSortOrder.property); |
||||
|
this.sort.active = sortable.id; |
||||
|
this.sort.direction = this.entitiesTableConfig.defaultSortOrder.direction === Direction.ASC ? 'asc' : 'desc'; |
||||
|
if (update) { |
||||
|
this.updateData(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
cellContent(entity: BaseData<HasId>, column: EntityTableColumn<BaseData<HasId>>) { |
||||
|
return this.domSanitizer.bypassSecurityTrustHtml(column.cellContentFunction(entity, column.key)); |
||||
|
} |
||||
|
|
||||
|
cellStyle(entity: BaseData<HasId>, column: EntityTableColumn<BaseData<HasId>>) { |
||||
|
return {...column.cellStyleFunction(entity, column.key), ...{maxWidth: column.maxWidth}}; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,28 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2019 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { BaseData, HasId } from '@shared/models/base-data'; |
||||
|
import { EntityTableConfig } from '@shared/components/entity/entities-table-config.models'; |
||||
|
|
||||
|
export interface AddEntityDialogData<T extends BaseData<HasId>> { |
||||
|
entitiesTableConfig: EntityTableConfig<T>; |
||||
|
} |
||||
|
|
||||
|
export interface EntityAction<T extends BaseData<HasId>> { |
||||
|
event: Event; |
||||
|
action: string; |
||||
|
entity: T; |
||||
|
} |
||||
@ -0,0 +1,43 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2019 The Thingsboard Authors |
||||
|
|
||||
|
Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
you may not use this file except in compliance with the License. |
||||
|
You may obtain a copy of the License at |
||||
|
|
||||
|
http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
|
||||
|
Unless required by applicable law or agreed to in writing, software |
||||
|
distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
See the License for the specific language governing permissions and |
||||
|
limitations under the License. |
||||
|
|
||||
|
--> |
||||
|
<tb-details-panel fxFlex |
||||
|
[headerTitle]="entity?.name" |
||||
|
headerSubtitle="{{ translations.details | translate }}" |
||||
|
[isReadOnly]="entitiesTableConfig.detailsReadonly(entity)" |
||||
|
[isEdit]="isEditValue" |
||||
|
(closeDetails)="onCloseEntityDetails()" |
||||
|
(toggleDetailsEditMode)="onToggleEditMode($event)" |
||||
|
(applyDetails)="saveEntity()" |
||||
|
[theForm]="detailsForm"> |
||||
|
<div class="details-buttons"> |
||||
|
<div [tb-help]="resources.helpLinkId"></div> |
||||
|
</div> |
||||
|
<mat-tab-group class="tb-absolute-fill" [ngClass]="{'tb-headless': isEditValue}" fxFlex [(selectedIndex)]="selectedTab"> |
||||
|
<mat-tab label="{{ 'details.details' | translate }}"> |
||||
|
<tb-anchor #entityDetailsForm></tb-anchor> |
||||
|
</mat-tab> |
||||
|
<!--mat-tab *ngIf="entity && entitiesTableConfig.entityType !== entityTypes.CUSTOMER" |
||||
|
label="{{ 'audit-log.audit-logs' | translate }}"> |
||||
|
<tb-audit-log-table [active]="selectedTab === 1" [auditLogMode]="auditLogModes.ENTITY" [entityId]="entity.id" detailsMode="true"></tb-audit-log-table> |
||||
|
</mat-tab> |
||||
|
<mat-tab *ngIf="entity && entitiesTableConfig.entityType === entityTypes.CUSTOMER" |
||||
|
label="{{ 'audit-log.audit-logs' | translate }}"> |
||||
|
<tb-audit-log-table [active]="selectedTab === 1" [auditLogMode]="auditLogModes.CUSTOMER" [customerId]="entity.id" detailsMode="true"></tb-audit-log-table> |
||||
|
</mat-tab--> |
||||
|
</mat-tab-group> |
||||
|
</tb-details-panel> |
||||
@ -0,0 +1,31 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2019 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
:host { |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
} |
||||
|
|
||||
|
:host ::ng-deep { |
||||
|
.mat-tab-body-wrapper { |
||||
|
position: absolute; |
||||
|
top: 49px; |
||||
|
left: 0; |
||||
|
right: 0; |
||||
|
bottom: 0; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,165 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2019 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { |
||||
|
Component, |
||||
|
ComponentFactoryResolver, |
||||
|
EventEmitter, |
||||
|
Input, |
||||
|
OnDestroy, |
||||
|
OnInit, |
||||
|
Output, |
||||
|
ViewChild |
||||
|
} from '@angular/core'; |
||||
|
import { PageComponent } from '@shared/components/page.component'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { EntityTableConfig } from '@shared/components/entity/entities-table-config.models'; |
||||
|
import { BaseData, HasId } from '@shared/models/base-data'; |
||||
|
import { |
||||
|
EntityType, |
||||
|
EntityTypeResource, |
||||
|
EntityTypeTranslation |
||||
|
} from '@shared/models/entity-type.models'; |
||||
|
import { NgForm } from '@angular/forms'; |
||||
|
import { EntityComponent } from '@shared/components/entity/entity.component'; |
||||
|
import { TbAnchorComponent } from '@shared/components/tb-anchor.component'; |
||||
|
import { EntityAction } from '@shared/components/entity/entity-component.models'; |
||||
|
import { Subscription } from 'rxjs'; |
||||
|
// import { AuditLogMode } from '@shared/models/audit-log.models';
|
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-entity-details-panel', |
||||
|
templateUrl: './entity-details-panel.component.html', |
||||
|
styleUrls: ['./entity-details-panel.component.scss'] |
||||
|
}) |
||||
|
export class EntityDetailsPanelComponent extends PageComponent implements OnInit, OnDestroy { |
||||
|
|
||||
|
@Input() entitiesTableConfig: EntityTableConfig<BaseData<HasId>>; |
||||
|
|
||||
|
@Output() |
||||
|
closeEntityDetails = new EventEmitter<void>(); |
||||
|
|
||||
|
@Output() |
||||
|
entityUpdated = new EventEmitter<BaseData<HasId>>(); |
||||
|
|
||||
|
@Output() |
||||
|
entityAction = new EventEmitter<EntityAction<BaseData<HasId>>>(); |
||||
|
|
||||
|
entityComponent: EntityComponent<BaseData<HasId>>; |
||||
|
detailsForm: NgForm; |
||||
|
|
||||
|
isEditValue = false; |
||||
|
selectedTab = 0; |
||||
|
|
||||
|
entityTypes = EntityType; |
||||
|
|
||||
|
@ViewChild('entityDetailsForm', {static: true}) entityDetailsFormAnchor: TbAnchorComponent; |
||||
|
|
||||
|
translations: EntityTypeTranslation; |
||||
|
resources: EntityTypeResource; |
||||
|
entity: BaseData<HasId>; |
||||
|
|
||||
|
private currentEntityId: HasId; |
||||
|
private entityActionSubscription: Subscription; |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
private componentFactoryResolver: ComponentFactoryResolver) { |
||||
|
super(store); |
||||
|
} |
||||
|
|
||||
|
@Input() |
||||
|
set entityId(entityId: HasId) { |
||||
|
if (entityId && entityId !== this.currentEntityId) { |
||||
|
this.currentEntityId = entityId; |
||||
|
this.reload(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
set isEdit(val: boolean) { |
||||
|
this.isEditValue = val; |
||||
|
this.entityComponent.isEdit = val; |
||||
|
} |
||||
|
|
||||
|
get isEdit() { |
||||
|
return this.isEditValue; |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
this.translations = this.entitiesTableConfig.entityTranslations; |
||||
|
this.resources = this.entitiesTableConfig.entityResources; |
||||
|
this.buildEntityComponent(); |
||||
|
} |
||||
|
|
||||
|
ngOnDestroy(): void { |
||||
|
super.ngOnDestroy(); |
||||
|
if (this.entityActionSubscription) { |
||||
|
this.entityActionSubscription.unsubscribe(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
buildEntityComponent() { |
||||
|
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.entitiesTableConfig.entityComponent); |
||||
|
const viewContainerRef = this.entityDetailsFormAnchor.viewContainerRef; |
||||
|
viewContainerRef.clear(); |
||||
|
const componentRef = viewContainerRef.createComponent(componentFactory); |
||||
|
this.entityComponent = componentRef.instance; |
||||
|
this.entityComponent.isEdit = this.isEdit; |
||||
|
this.entityComponent.entitiesTableConfig = this.entitiesTableConfig; |
||||
|
this.detailsForm = this.entityComponent.entityNgForm; |
||||
|
this.entityActionSubscription = this.entityComponent.entityAction.subscribe((action) => { |
||||
|
this.entityAction.emit(action); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
reload(): void { |
||||
|
this.isEdit = false; |
||||
|
this.entitiesTableConfig.loadEntity(this.currentEntityId).subscribe( |
||||
|
(entity) => { |
||||
|
this.entity = entity; |
||||
|
this.entityComponent.entity = entity; |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
onCloseEntityDetails() { |
||||
|
this.closeEntityDetails.emit(); |
||||
|
} |
||||
|
|
||||
|
onToggleEditMode(isEdit: boolean) { |
||||
|
this.isEdit = isEdit; |
||||
|
if (!this.isEdit) { |
||||
|
this.entityComponent.entity = this.entity; |
||||
|
} else { |
||||
|
this.selectedTab = 0; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
saveEntity() { |
||||
|
if (this.detailsForm.valid) { |
||||
|
const editingEntity = {...this.entity, ...this.entityComponent.entityFormValue()}; |
||||
|
this.entitiesTableConfig.saveEntity(editingEntity).subscribe( |
||||
|
(entity) => { |
||||
|
this.entity = entity; |
||||
|
this.entityComponent.entity = entity; |
||||
|
this.isEdit = false; |
||||
|
this.entityUpdated.emit(this.entity); |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,36 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2019 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { BaseData, HasId } from '@shared/models/base-data'; |
||||
|
import { PageComponent } from '@shared/components/page.component'; |
||||
|
import { Input, OnInit } from '@angular/core'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { EntityTableConfig } from '@shared/components/entity/entities-table-config.models'; |
||||
|
|
||||
|
export abstract class EntityTableHeaderComponent<T extends BaseData<HasId>> extends PageComponent implements OnInit { |
||||
|
|
||||
|
@Input() |
||||
|
entitiesTableConfig: EntityTableConfig<T>; |
||||
|
|
||||
|
protected constructor(protected store: Store<AppState>) { |
||||
|
super(store); |
||||
|
} |
||||
|
|
||||
|
ngOnInit() { |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,110 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2019 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { BaseData, HasId } from '@shared/models/base-data'; |
||||
|
import { FormGroup, NgForm } from '@angular/forms'; |
||||
|
import { PageComponent } from '@shared/components/page.component'; |
||||
|
import { EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { EntityAction } from '@shared/components/entity/entity-component.models'; |
||||
|
import { EntityTableConfig } from '@shared/components/entity/entities-table-config.models'; |
||||
|
|
||||
|
export abstract class EntityComponent<T extends BaseData<HasId>> extends PageComponent implements OnInit { |
||||
|
|
||||
|
entityValue: T; |
||||
|
entityForm: FormGroup; |
||||
|
|
||||
|
@ViewChild('entityNgForm', {static: true}) entityNgForm: NgForm; |
||||
|
|
||||
|
isEditValue: boolean; |
||||
|
|
||||
|
@Input() |
||||
|
set isEdit(isEdit: boolean) { |
||||
|
this.isEditValue = isEdit; |
||||
|
this.updateFormState(); |
||||
|
} |
||||
|
|
||||
|
get isEdit() { |
||||
|
return this.isEditValue; |
||||
|
} |
||||
|
|
||||
|
get isAdd(): boolean { |
||||
|
return this.entityValue && !this.entityValue.id; |
||||
|
} |
||||
|
|
||||
|
@Input() |
||||
|
set entity(entity: T) { |
||||
|
this.entityValue = entity; |
||||
|
if (this.entityForm) { |
||||
|
this.entityForm.reset(); |
||||
|
this.updateForm(entity); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
get entity(): T { |
||||
|
return this.entityValue; |
||||
|
} |
||||
|
|
||||
|
@Input() |
||||
|
entitiesTableConfig: EntityTableConfig<T>; |
||||
|
|
||||
|
@Output() |
||||
|
entityAction = new EventEmitter<EntityAction<T>>(); |
||||
|
|
||||
|
protected constructor(protected store: Store<AppState>) { |
||||
|
super(store); |
||||
|
} |
||||
|
|
||||
|
ngOnInit() { |
||||
|
this.entityForm = this.buildForm(this.entityValue); |
||||
|
} |
||||
|
|
||||
|
onEntityAction($event: Event, action: string) { |
||||
|
const entityAction = {event: $event, action, entity: this.entity} as EntityAction<T>; |
||||
|
let handled = false; |
||||
|
if (this.entitiesTableConfig) { |
||||
|
handled = this.entitiesTableConfig.onEntityAction(entityAction); |
||||
|
} |
||||
|
if (!handled) { |
||||
|
this.entityAction.emit(entityAction); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
updateFormState() { |
||||
|
if (this.entityForm) { |
||||
|
if (this.isEditValue) { |
||||
|
this.entityForm.enable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.entityForm.disable({emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
entityFormValue() { |
||||
|
const formValue = this.entityForm ? {...this.entityForm.value} : {}; |
||||
|
return this.prepareFormValue(formValue); |
||||
|
} |
||||
|
|
||||
|
prepareFormValue(formValue: any): any { |
||||
|
return formValue; |
||||
|
} |
||||
|
|
||||
|
abstract buildForm(entity: T): FormGroup; |
||||
|
|
||||
|
abstract updateForm(entity: T); |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,47 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2019 The Thingsboard Authors |
||||
|
|
||||
|
Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
you may not use this file except in compliance with the License. |
||||
|
You may obtain a copy of the License at |
||||
|
|
||||
|
http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
|
||||
|
Unless required by applicable law or agreed to in writing, software |
||||
|
distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
See the License for the specific language governing permissions and |
||||
|
limitations under the License. |
||||
|
|
||||
|
--> |
||||
|
<section fxLayout="column" fxLayoutAlign="start start"> |
||||
|
<section fxLayout="row" fxLayoutAlign="start start" fxLayoutGap="16px"> |
||||
|
<mat-form-field> |
||||
|
<mat-placeholder translate>datetime.date-from</mat-placeholder> |
||||
|
<mat-datetimepicker-toggle [for]="startDatePicker" matPrefix></mat-datetimepicker-toggle> |
||||
|
<mat-datetimepicker #startDatePicker type="date" openOnFocus="true"></mat-datetimepicker> |
||||
|
<input matInput [(ngModel)]="startDate" [matDatetimepicker]="startDatePicker" (ngModelChange)="onStartDateChange()"> |
||||
|
</mat-form-field> |
||||
|
<mat-form-field> |
||||
|
<mat-placeholder translate>datetime.time-from</mat-placeholder> |
||||
|
<mat-datetimepicker-toggle [for]="startTimePicker" matPrefix></mat-datetimepicker-toggle> |
||||
|
<mat-datetimepicker #startTimePicker type="time" openOnFocus="true"></mat-datetimepicker> |
||||
|
<input matInput [(ngModel)]="startDate" [matDatetimepicker]="startTimePicker" (ngModelChange)="onStartDateChange()"> |
||||
|
</mat-form-field> |
||||
|
</section> |
||||
|
<section fxLayout="row" fxLayoutAlign="start start" fxLayoutGap="16px"> |
||||
|
<mat-form-field> |
||||
|
<mat-placeholder translate>datetime.date-to</mat-placeholder> |
||||
|
<mat-datetimepicker-toggle [for]="endDatePicker" matPrefix></mat-datetimepicker-toggle> |
||||
|
<mat-datetimepicker #endDatePicker type="date" openOnFocus="true"></mat-datetimepicker> |
||||
|
<input matInput [(ngModel)]="endDate" [matDatetimepicker]="endDatePicker" (ngModelChange)="onEndDateChange()"> |
||||
|
</mat-form-field> |
||||
|
<mat-form-field> |
||||
|
<mat-placeholder translate>datetime.time-to</mat-placeholder> |
||||
|
<mat-datetimepicker-toggle [for]="endTimePicker" matPrefix></mat-datetimepicker-toggle> |
||||
|
<mat-datetimepicker #endTimePicker type="time" openOnFocus="true"></mat-datetimepicker> |
||||
|
<input matInput [(ngModel)]="endDate" [matDatetimepicker]="endTimePicker" (ngModelChange)="onEndDateChange()"> |
||||
|
</mat-form-field> |
||||
|
</section> |
||||
|
</section> |
||||
@ -0,0 +1,26 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2019 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
:host ::ng-deep { |
||||
|
.mat-form-field-wrapper { |
||||
|
padding-bottom: 8px; |
||||
|
} |
||||
|
.mat-form-field-underline { |
||||
|
bottom: 8px; |
||||
|
} |
||||
|
.mat-form-field-infix { |
||||
|
width: 150px; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,137 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2019 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { Component, forwardRef, Input, OnInit } from '@angular/core'; |
||||
|
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; |
||||
|
import { FixedWindow } from '@shared/models/time/time.models'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-datetime-period', |
||||
|
templateUrl: './datetime-period.component.html', |
||||
|
styleUrls: ['./datetime-period.component.scss'], |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => DatetimePeriodComponent), |
||||
|
multi: true |
||||
|
} |
||||
|
] |
||||
|
}) |
||||
|
export class DatetimePeriodComponent implements OnInit, ControlValueAccessor { |
||||
|
|
||||
|
@Input() disabled: boolean; |
||||
|
|
||||
|
modelValue: FixedWindow; |
||||
|
|
||||
|
startDate: Date; |
||||
|
endDate: Date; |
||||
|
|
||||
|
endTime: any; |
||||
|
|
||||
|
maxStartDate: Date; |
||||
|
minEndDate: Date; |
||||
|
maxEndDate: Date; |
||||
|
|
||||
|
changePending = false; |
||||
|
|
||||
|
private propagateChange = null; |
||||
|
|
||||
|
constructor() { |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: any): void { |
||||
|
this.propagateChange = fn; |
||||
|
if (this.changePending && this.propagateChange) { |
||||
|
this.changePending = false; |
||||
|
this.propagateChange(this.modelValue); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(fn: any): void { |
||||
|
} |
||||
|
|
||||
|
setDisabledState(isDisabled: boolean): void { |
||||
|
this.disabled = isDisabled; |
||||
|
} |
||||
|
|
||||
|
writeValue(datePeriod: FixedWindow): void { |
||||
|
this.modelValue = datePeriod; |
||||
|
if (this.modelValue) { |
||||
|
this.startDate = new Date(this.modelValue.startTimeMs); |
||||
|
this.endDate = new Date(this.modelValue.endTimeMs); |
||||
|
} else { |
||||
|
const date = new Date(); |
||||
|
this.startDate = new Date( |
||||
|
date.getFullYear(), |
||||
|
date.getMonth(), |
||||
|
date.getDate() - 1, |
||||
|
date.getHours(), |
||||
|
date.getMinutes(), |
||||
|
date.getSeconds(), |
||||
|
date.getMilliseconds()); |
||||
|
this.endDate = date; |
||||
|
this.updateView(); |
||||
|
} |
||||
|
this.updateMinMaxDates(); |
||||
|
} |
||||
|
|
||||
|
updateView() { |
||||
|
let value: FixedWindow = null; |
||||
|
if (this.startDate && this.endDate) { |
||||
|
value = new FixedWindow(); |
||||
|
value.startTimeMs = this.startDate.getTime(); |
||||
|
value.endTimeMs = this.endDate.getTime(); |
||||
|
} |
||||
|
this.modelValue = value; |
||||
|
if (!this.propagateChange) { |
||||
|
this.changePending = true; |
||||
|
} else { |
||||
|
this.propagateChange(this.modelValue); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
updateMinMaxDates() { |
||||
|
this.maxStartDate = new Date(this.endDate.getTime() - 1000); |
||||
|
this.minEndDate = new Date(this.startDate.getTime() + 1000); |
||||
|
this.maxEndDate = new Date(); |
||||
|
} |
||||
|
|
||||
|
onStartDateChange() { |
||||
|
if (this.startDate) { |
||||
|
if (this.startDate.getTime() > this.maxStartDate.getTime()) { |
||||
|
this.startDate = new Date(this.maxStartDate.getTime()); |
||||
|
} |
||||
|
this.updateMinMaxDates(); |
||||
|
} |
||||
|
this.updateView(); |
||||
|
} |
||||
|
|
||||
|
onEndDateChange() { |
||||
|
if (this.endDate) { |
||||
|
if (this.endDate.getTime() < this.minEndDate.getTime()) { |
||||
|
this.endDate = new Date(this.minEndDate.getTime()); |
||||
|
} else if (this.endDate.getTime() > this.maxEndDate.getTime()) { |
||||
|
this.endDate = new Date(this.maxEndDate.getTime()); |
||||
|
} |
||||
|
this.updateMinMaxDates(); |
||||
|
} |
||||
|
this.updateView(); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,54 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2019 The Thingsboard Authors |
||||
|
|
||||
|
Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
you may not use this file except in compliance with the License. |
||||
|
You may obtain a copy of the License at |
||||
|
|
||||
|
http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
|
||||
|
Unless required by applicable law or agreed to in writing, software |
||||
|
distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
See the License for the specific language governing permissions and |
||||
|
limitations under the License. |
||||
|
|
||||
|
--> |
||||
|
<section fxLayout="row"> |
||||
|
<section class="interval-section" fxLayout="column" fxFlex [fxShow]="advanced"> |
||||
|
<label class="tb-small interval-label" translate>{{ predefinedName }}</label> |
||||
|
<section fxLayout="row" fxLayoutAlign="start start" fxFlex fxLayoutGap="6px"> |
||||
|
<mat-form-field class="number-input"> |
||||
|
<mat-label translate>timeinterval.days</mat-label> |
||||
|
<input matInput type="number" step="1" min="0" [(ngModel)]="days" (ngModelChange)="onTimeInputChange('days')"/> |
||||
|
</mat-form-field> |
||||
|
<mat-form-field class="number-input"> |
||||
|
<mat-label translate>timeinterval.hours</mat-label> |
||||
|
<input matInput type="number" step="1" [(ngModel)]="hours" (ngModelChange)="onTimeInputChange('hours')"/> |
||||
|
</mat-form-field> |
||||
|
<mat-form-field class="number-input"> |
||||
|
<mat-label translate>timeinterval.minutes</mat-label> |
||||
|
<input matInput type="number" step="1" [(ngModel)]="mins" (ngModelChange)="onTimeInputChange('mins')"/> |
||||
|
</mat-form-field> |
||||
|
<mat-form-field class="number-input"> |
||||
|
<mat-label translate>timeinterval.seconds</mat-label> |
||||
|
<input matInput type="number" step="1" [(ngModel)]="secs" (ngModelChange)="onTimeInputChange('secs')"/> |
||||
|
</mat-form-field> |
||||
|
</section> |
||||
|
</section> |
||||
|
<section class="interval-section" fxLayout="row" fxFlex [fxShow]="!advanced"> |
||||
|
<mat-form-field fxFlex> |
||||
|
<mat-label translate>{{ predefinedName }}</mat-label> |
||||
|
<mat-select matInput [(ngModel)]="intervalMs" (ngModelChange)="onIntervalMsChange()" style="min-width: 150px;"> |
||||
|
<mat-option *ngFor="let interval of intervals" [value]="interval.value"> |
||||
|
{{ interval.name | translate:interval.translateParams }} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
</section> |
||||
|
<section fxLayout="column" fxLayoutAlign="center center"> |
||||
|
<label class="tb-small advanced-label" translate>timeinterval.advanced</label> |
||||
|
<mat-slide-toggle class="advanced-switch" [(ngModel)]="advanced" (ngModelChange)="onAdvancedChange()"></mat-slide-toggle> |
||||
|
</section> |
||||
|
</section> |
||||
@ -0,0 +1,43 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2019 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
:host { |
||||
|
min-width: 355px; |
||||
|
|
||||
|
.advanced-switch { |
||||
|
margin-bottom: 16px; |
||||
|
} |
||||
|
|
||||
|
.advanced-label { |
||||
|
margin: 5px 0; |
||||
|
} |
||||
|
|
||||
|
.interval-section { |
||||
|
min-height: 66px; |
||||
|
.interval-label { |
||||
|
margin-bottom: 7px; |
||||
|
margin-top: -1px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
:host ::ng-deep { |
||||
|
.number-input { |
||||
|
.mat-form-field-infix { |
||||
|
width: 70px; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,277 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2019 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { ChangeDetectorRef, Component, forwardRef, Input, OnInit } from '@angular/core'; |
||||
|
import { |
||||
|
ControlValueAccessor, |
||||
|
FormControl, |
||||
|
NG_VALIDATORS, |
||||
|
NG_VALUE_ACCESSOR, |
||||
|
Validator |
||||
|
} from '@angular/forms'; |
||||
|
import { Timewindow } from '@shared/models/time/time.models'; |
||||
|
import { TimeInterval, TimeService } from '@core/services/time.service'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-timeinterval', |
||||
|
templateUrl: './timeinterval.component.html', |
||||
|
styleUrls: ['./timeinterval.component.scss'], |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => TimeintervalComponent), |
||||
|
multi: true |
||||
|
} |
||||
|
] |
||||
|
}) |
||||
|
export class TimeintervalComponent implements OnInit, ControlValueAccessor { |
||||
|
|
||||
|
minValue: number; |
||||
|
maxValue: number; |
||||
|
|
||||
|
@Input() |
||||
|
set min(min: number) { |
||||
|
if (typeof min !== 'undefined' && min !== this.minValue) { |
||||
|
this.minValue = min; |
||||
|
this.updateView(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Input() |
||||
|
set max(max: number) { |
||||
|
if (typeof max !== 'undefined' && max !== this.maxValue) { |
||||
|
this.maxValue = max; |
||||
|
this.updateView(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Input() predefinedName: string; |
||||
|
@Input() disabled: boolean; |
||||
|
|
||||
|
days = 0; |
||||
|
hours = 0; |
||||
|
mins = 1; |
||||
|
secs = 0; |
||||
|
|
||||
|
intervalMs = 0; |
||||
|
modelValue: number; |
||||
|
|
||||
|
advanced = false; |
||||
|
rendered = false; |
||||
|
|
||||
|
intervals: Array<TimeInterval>; |
||||
|
|
||||
|
private propagateChange = (_: any) => {}; |
||||
|
|
||||
|
constructor(private timeService: TimeService) { |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
this.boundInterval(); |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: any): void { |
||||
|
this.propagateChange = fn; |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(fn: any): void { |
||||
|
} |
||||
|
|
||||
|
setDisabledState(isDisabled: boolean): void { |
||||
|
this.disabled = isDisabled; |
||||
|
} |
||||
|
|
||||
|
writeValue(intervalMs: number): void { |
||||
|
this.modelValue = intervalMs; |
||||
|
this.rendered = true; |
||||
|
if (typeof this.modelValue !== 'undefined') { |
||||
|
const min = this.timeService.boundMinInterval(this.minValue); |
||||
|
const max = this.timeService.boundMaxInterval(this.maxValue); |
||||
|
if (this.modelValue >= min && this.modelValue <= max) { |
||||
|
this.advanced = !this.timeService.matchesExistingInterval(this.minValue, this.maxValue, this.modelValue); |
||||
|
this.setIntervalMs(this.modelValue); |
||||
|
} else { |
||||
|
this.boundInterval(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
setIntervalMs(intervalMs: number) { |
||||
|
if (!this.advanced) { |
||||
|
this.intervalMs = intervalMs; |
||||
|
} |
||||
|
const intervalSeconds = Math.floor(intervalMs / 1000); |
||||
|
this.days = Math.floor(intervalSeconds / 86400); |
||||
|
this.hours = Math.floor((intervalSeconds % 86400) / 3600); |
||||
|
this.mins = Math.floor(((intervalSeconds % 86400) % 3600) / 60); |
||||
|
this.secs = intervalSeconds % 60; |
||||
|
} |
||||
|
|
||||
|
boundInterval() { |
||||
|
const min = this.timeService.boundMinInterval(this.minValue); |
||||
|
const max = this.timeService.boundMaxInterval(this.maxValue); |
||||
|
this.intervals = this.timeService.getIntervals(this.minValue, this.maxValue); |
||||
|
if (this.rendered) { |
||||
|
let newIntervalMs = this.modelValue; |
||||
|
if (newIntervalMs < min) { |
||||
|
newIntervalMs = min; |
||||
|
} else if (newIntervalMs > max) { |
||||
|
newIntervalMs = max; |
||||
|
} |
||||
|
if (!this.advanced) { |
||||
|
newIntervalMs = this.timeService.boundToPredefinedInterval(min, max, newIntervalMs); |
||||
|
} |
||||
|
if (newIntervalMs !== this.modelValue) { |
||||
|
this.setIntervalMs(newIntervalMs); |
||||
|
this.updateView(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
updateView() { |
||||
|
if (!this.rendered) { |
||||
|
return; |
||||
|
} |
||||
|
let value = null; |
||||
|
let intervalMs; |
||||
|
if (!this.advanced) { |
||||
|
intervalMs = this.intervalMs; |
||||
|
if (!intervalMs || isNaN(intervalMs)) { |
||||
|
intervalMs = this.calculateIntervalMs(); |
||||
|
} |
||||
|
} else { |
||||
|
intervalMs = this.calculateIntervalMs(); |
||||
|
} |
||||
|
if (!isNaN(intervalMs) && intervalMs > 0) { |
||||
|
value = intervalMs; |
||||
|
} |
||||
|
this.modelValue = value; |
||||
|
this.propagateChange(this.modelValue); |
||||
|
this.boundInterval(); |
||||
|
} |
||||
|
|
||||
|
calculateIntervalMs(): number { |
||||
|
return (this.days * 86400 + |
||||
|
this.hours * 3600 + |
||||
|
this.mins * 60 + |
||||
|
this.secs) * 1000; |
||||
|
} |
||||
|
|
||||
|
onIntervalMsChange() { |
||||
|
this.updateView(); |
||||
|
} |
||||
|
|
||||
|
onAdvancedChange() { |
||||
|
if (!this.advanced) { |
||||
|
this.intervalMs = this.calculateIntervalMs(); |
||||
|
} else { |
||||
|
let intervalMs = this.intervalMs; |
||||
|
if (!intervalMs || isNaN(intervalMs)) { |
||||
|
intervalMs = this.calculateIntervalMs(); |
||||
|
} |
||||
|
this.setIntervalMs(intervalMs); |
||||
|
} |
||||
|
this.updateView(); |
||||
|
} |
||||
|
|
||||
|
onTimeInputChange(type: string) { |
||||
|
switch (type) { |
||||
|
case 'secs': |
||||
|
setTimeout(() => this.onSecsChange(), 0); |
||||
|
break; |
||||
|
case 'mins': |
||||
|
setTimeout(() => this.onMinsChange(), 0); |
||||
|
break; |
||||
|
case 'hours': |
||||
|
setTimeout(() => this.onHoursChange(), 0); |
||||
|
break; |
||||
|
case 'days': |
||||
|
setTimeout(() => this.onDaysChange(), 0); |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
onSecsChange() { |
||||
|
if (typeof this.secs === 'undefined') { |
||||
|
return; |
||||
|
} |
||||
|
if (this.secs < 0) { |
||||
|
if ((this.days + this.hours + this.mins) > 0) { |
||||
|
this.secs = this.secs + 60; |
||||
|
this.mins--; |
||||
|
this.onMinsChange(); |
||||
|
} else { |
||||
|
this.secs = 0; |
||||
|
} |
||||
|
} else if (this.secs >= 60) { |
||||
|
this.secs = this.secs - 60; |
||||
|
this.mins++; |
||||
|
this.onMinsChange(); |
||||
|
} |
||||
|
this.updateView(); |
||||
|
} |
||||
|
|
||||
|
onMinsChange() { |
||||
|
if (typeof this.mins === 'undefined') { |
||||
|
return; |
||||
|
} |
||||
|
if (this.mins < 0) { |
||||
|
if ((this.days + this.hours) > 0) { |
||||
|
this.mins = this.mins + 60; |
||||
|
this.hours--; |
||||
|
this.onHoursChange(); |
||||
|
} else { |
||||
|
this.mins = 0; |
||||
|
} |
||||
|
} else if (this.mins >= 60) { |
||||
|
this.mins = this.mins - 60; |
||||
|
this.hours++; |
||||
|
this.onHoursChange(); |
||||
|
} |
||||
|
this.updateView(); |
||||
|
} |
||||
|
|
||||
|
onHoursChange() { |
||||
|
if (typeof this.hours === 'undefined') { |
||||
|
return; |
||||
|
} |
||||
|
if (this.hours < 0) { |
||||
|
if (this.days > 0) { |
||||
|
this.hours = this.hours + 24; |
||||
|
this.days--; |
||||
|
this.onDaysChange(); |
||||
|
} else { |
||||
|
this.hours = 0; |
||||
|
} |
||||
|
} else if (this.hours >= 24) { |
||||
|
this.hours = this.hours - 24; |
||||
|
this.days++; |
||||
|
this.onDaysChange(); |
||||
|
} |
||||
|
this.updateView(); |
||||
|
} |
||||
|
|
||||
|
onDaysChange() { |
||||
|
if (typeof this.days === 'undefined') { |
||||
|
return; |
||||
|
} |
||||
|
if (this.days < 0) { |
||||
|
this.days = 0; |
||||
|
} |
||||
|
this.updateView(); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,127 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2019 The Thingsboard Authors |
||||
|
|
||||
|
Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
you may not use this file except in compliance with the License. |
||||
|
You may obtain a copy of the License at |
||||
|
|
||||
|
http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
|
||||
|
Unless required by applicable law or agreed to in writing, software |
||||
|
distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
See the License for the specific language governing permissions and |
||||
|
limitations under the License. |
||||
|
|
||||
|
--> |
||||
|
<form [formGroup]="timewindowForm" (ngSubmit)="update()"> |
||||
|
<fieldset [disabled]="(isLoading$ | async)"> |
||||
|
<div class="mat-content" style="height: 100%;" fxFlex fxLayout="column"> |
||||
|
<section fxLayout="column"> |
||||
|
<mat-tab-group dynamicHeight [ngClass]="{'tb-headless': historyOnly}" |
||||
|
(selectedIndexChange)="timewindowForm.markAsDirty()" [(selectedIndex)]="timewindow.selectedTab"> |
||||
|
<mat-tab label="{{ 'timewindow.realtime' | translate }}"> |
||||
|
<div formGroupName="realtime" class="mat-content mat-padding" fxLayout="column"> |
||||
|
<tb-timeinterval |
||||
|
formControlName="timewindowMs" |
||||
|
predefinedName="timewindow.last" |
||||
|
[required]="timewindow.selectedTab === timewindowTypes.REALTIME" |
||||
|
style="padding-top: 8px;"></tb-timeinterval> |
||||
|
</div> |
||||
|
</mat-tab> |
||||
|
<mat-tab label="{{ 'timewindow.history' | translate }}"> |
||||
|
<div formGroupName="history" class="mat-content mat-padding" style="padding-top: 8px;"> |
||||
|
<mat-radio-group formControlName="historyType"> |
||||
|
<mat-radio-button [value]="historyTypes.LAST_INTERVAL" color="primary"> |
||||
|
<section fxLayout="column"> |
||||
|
<tb-timeinterval |
||||
|
formControlName="timewindowMs" |
||||
|
predefinedName="timewindow.last" |
||||
|
[fxShow]="timewindowForm.get('history').get('historyType').value === historyTypes.LAST_INTERVAL" |
||||
|
[required]="timewindow.selectedTab === timewindowTypes.HISTORY && |
||||
|
timewindowForm.get('history').get('historyType').value === historyTypes.LAST_INTERVAL" |
||||
|
style="padding-top: 8px;"></tb-timeinterval> |
||||
|
</section> |
||||
|
</mat-radio-button> |
||||
|
<mat-radio-button [value]="historyTypes.FIXED" color="primary"> |
||||
|
<section fxLayout="column"> |
||||
|
<span translate>timewindow.time-period</span> |
||||
|
<tb-datetime-period |
||||
|
formControlName="fixedTimewindow" |
||||
|
[fxShow]="timewindowForm.get('history').get('historyType').value === historyTypes.FIXED" |
||||
|
[required]="timewindow.selectedTab === timewindowTypes.HISTORY && |
||||
|
timewindowForm.get('history').get('historyType').value === historyTypes.FIXED" |
||||
|
style="padding-top: 8px;"></tb-datetime-period> |
||||
|
</section> |
||||
|
</mat-radio-button> |
||||
|
</mat-radio-group> |
||||
|
</div> |
||||
|
</mat-tab> |
||||
|
</mat-tab-group> |
||||
|
<div *ngIf="aggregation" formGroupName="aggregation" class="mat-content mat-padding" fxLayout="column"> |
||||
|
<mat-form-field> |
||||
|
<mat-label translate>aggregation.function</mat-label> |
||||
|
<mat-select matInput formControlName="type" style="min-width: 150px;"> |
||||
|
<mat-option *ngFor="let aggregation of aggregations" [value]="aggregation"> |
||||
|
{{ aggregationTypesTranslations.get(aggregation) | translate }} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
<div *ngIf="timewindowForm.get('aggregation').get('type').value === aggregationTypes.NONE" |
||||
|
class="limit-slider-container" |
||||
|
fxLayout="row" fxLayoutAlign="start center"> |
||||
|
<span translate>aggregation.limit</span> |
||||
|
<mat-slider fxFlex formControlName="limit" |
||||
|
thumbLabel |
||||
|
[value]="timewindowForm.get('aggregation').get('limit').value" |
||||
|
min="{{minDatapointsLimit()}}" |
||||
|
max="{{maxDatapointsLimit()}}"> |
||||
|
</mat-slider> |
||||
|
<mat-form-field style="max-width: 80px;"> |
||||
|
<input matInput formControlName="limit" type="number" step="1" |
||||
|
[value]="timewindowForm.get('aggregation').get('limit').value" |
||||
|
min="{{minDatapointsLimit()}}" |
||||
|
max="{{maxDatapointsLimit()}}"/> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div formGroupName="realtime" |
||||
|
*ngIf="aggregation && timewindowForm.get('aggregation').get('type').value !== aggregationTypes.NONE && |
||||
|
timewindow.selectedTab === timewindowTypes.REALTIME" class="mat-content mat-padding" fxLayout="column"> |
||||
|
<tb-timeinterval |
||||
|
formControlName="interval" |
||||
|
[min]="minRealtimeAggInterval()" [max]="maxRealtimeAggInterval()" |
||||
|
predefinedName="aggregation.group-interval"> |
||||
|
</tb-timeinterval> |
||||
|
</div> |
||||
|
<div formGroupName="history" |
||||
|
*ngIf="aggregation && timewindowForm.get('aggregation').get('type').value !== aggregationTypes.NONE && |
||||
|
timewindow.selectedTab === timewindowTypes.HISTORY" class="mat-content mat-padding" fxLayout="column"> |
||||
|
<tb-timeinterval |
||||
|
formControlName="interval" |
||||
|
[min]="minHistoryAggInterval()" [max]="maxHistoryAggInterval()" |
||||
|
predefinedName="aggregation.group-interval"> |
||||
|
</tb-timeinterval> |
||||
|
</div> |
||||
|
</section> |
||||
|
<span fxFlex></span> |
||||
|
<div fxLayout="row" class="tb-panel-actions"> |
||||
|
<span fxFlex></span> |
||||
|
<button type="submit" |
||||
|
mat-raised-button |
||||
|
color="primary" |
||||
|
[disabled]="(isLoading$ | async) || timewindowForm.invalid || !timewindowForm.dirty"> |
||||
|
{{ 'action.update' | translate }} |
||||
|
</button> |
||||
|
<button type="button" |
||||
|
mat-button |
||||
|
[disabled]="(isLoading$ | async)" |
||||
|
(click)="cancel()" |
||||
|
style="margin-right: 20px;"> |
||||
|
{{ 'action.cancel' | translate }} |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</fieldset> |
||||
|
</form> |
||||
@ -0,0 +1,63 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2019 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
:host { |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
form, |
||||
|
fieldset { |
||||
|
height: 100%; |
||||
|
} |
||||
|
|
||||
|
.mat-content { |
||||
|
overflow: hidden; |
||||
|
background-color: #fff; |
||||
|
} |
||||
|
|
||||
|
.mat-padding { |
||||
|
padding: 0 16px; |
||||
|
} |
||||
|
|
||||
|
.limit-slider-container { |
||||
|
>:first-child { |
||||
|
margin-right: 16px; |
||||
|
} |
||||
|
>:last-child { |
||||
|
margin-left: 16px; |
||||
|
} |
||||
|
>:first-child, >:last-child { |
||||
|
min-width: 25px; |
||||
|
max-width: 42px; |
||||
|
} |
||||
|
mat-form-field input[type=number] { |
||||
|
text-align: center; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
:host ::ng-deep { |
||||
|
mat-radio-button { |
||||
|
display: block; |
||||
|
margin-bottom: 16px; |
||||
|
.mat-radio-label { |
||||
|
width: 100%; |
||||
|
align-items: start; |
||||
|
.mat-radio-label-content { |
||||
|
width: 100%; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,203 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2019 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { |
||||
|
Component, |
||||
|
Inject, |
||||
|
InjectionToken, |
||||
|
OnInit, |
||||
|
ViewChild, |
||||
|
ViewContainerRef |
||||
|
} from '@angular/core'; |
||||
|
import { TranslateService } from '@ngx-translate/core'; |
||||
|
import { MillisecondsToTimeStringPipe } from '@shared/pipe/milliseconds-to-time-string.pipe'; |
||||
|
import { |
||||
|
Aggregation, |
||||
|
aggregationTranslations, |
||||
|
AggregationType, |
||||
|
HistoryWindow, |
||||
|
HistoryWindowType, |
||||
|
IntervalWindow, |
||||
|
Timewindow, |
||||
|
TimewindowType |
||||
|
} from '@shared/models/time/time.models'; |
||||
|
import { DatePipe } from '@angular/common'; |
||||
|
import { Overlay, OverlayRef } from '@angular/cdk/overlay'; |
||||
|
import { PageComponent } from '@shared/components/page.component'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; |
||||
|
import { TimeService } from '@core/services/time.service'; |
||||
|
|
||||
|
export const TIMEWINDOW_PANEL_DATA = new InjectionToken<any>('TimewindowPanelData'); |
||||
|
|
||||
|
export interface TimewindowPanelData { |
||||
|
historyOnly: boolean; |
||||
|
timewindow: Timewindow; |
||||
|
aggregation: boolean; |
||||
|
} |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-timewindow-panel', |
||||
|
templateUrl: './timewindow-panel.component.html', |
||||
|
styleUrls: ['./timewindow-panel.component.scss'] |
||||
|
}) |
||||
|
export class TimewindowPanelComponent extends PageComponent implements OnInit { |
||||
|
|
||||
|
historyOnly = false; |
||||
|
|
||||
|
aggregation = false; |
||||
|
|
||||
|
timewindow: Timewindow; |
||||
|
|
||||
|
result: Timewindow; |
||||
|
|
||||
|
timewindowForm: FormGroup; |
||||
|
|
||||
|
historyTypes = HistoryWindowType; |
||||
|
|
||||
|
timewindowTypes = TimewindowType; |
||||
|
|
||||
|
aggregationTypes = AggregationType; |
||||
|
|
||||
|
aggregations = Object.keys(AggregationType); |
||||
|
|
||||
|
aggregationTypesTranslations = aggregationTranslations; |
||||
|
|
||||
|
constructor(@Inject(TIMEWINDOW_PANEL_DATA) public data: TimewindowPanelData, |
||||
|
public overlayRef: OverlayRef, |
||||
|
protected store: Store<AppState>, |
||||
|
public fb: FormBuilder, |
||||
|
private timeService: TimeService, |
||||
|
private translate: TranslateService, |
||||
|
private millisecondsToTimeStringPipe: MillisecondsToTimeStringPipe, |
||||
|
private datePipe: DatePipe, |
||||
|
private overlay: Overlay, |
||||
|
public viewContainerRef: ViewContainerRef) { |
||||
|
super(store); |
||||
|
this.historyOnly = data.historyOnly; |
||||
|
this.timewindow = data.timewindow; |
||||
|
this.aggregation = data.aggregation; |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
this.timewindowForm = this.fb.group({ |
||||
|
realtime: this.fb.group( |
||||
|
{ |
||||
|
timewindowMs: [ |
||||
|
this.timewindow.realtime && typeof this.timewindow.realtime.timewindowMs !== 'undefined' |
||||
|
? this.timewindow.realtime.timewindowMs : null |
||||
|
], |
||||
|
interval: [ |
||||
|
this.timewindow.realtime && typeof this.timewindow.realtime.interval !== 'undefined' |
||||
|
? this.timewindow.realtime.interval : null |
||||
|
] |
||||
|
} |
||||
|
), |
||||
|
history: this.fb.group( |
||||
|
{ |
||||
|
historyType: [ |
||||
|
this.timewindow.history && typeof this.timewindow.history.historyType !== 'undefined' |
||||
|
? this.timewindow.history.historyType : HistoryWindowType.LAST_INTERVAL |
||||
|
], |
||||
|
timewindowMs: [ |
||||
|
this.timewindow.history && typeof this.timewindow.history.timewindowMs !== 'undefined' |
||||
|
? this.timewindow.history.timewindowMs : null |
||||
|
], |
||||
|
interval: [ |
||||
|
this.timewindow.history && typeof this.timewindow.history.interval !== 'undefined' |
||||
|
? this.timewindow.history.interval : null |
||||
|
], |
||||
|
fixedTimewindow: [ |
||||
|
this.timewindow.history && typeof this.timewindow.history.fixedTimewindow !== 'undefined' |
||||
|
? this.timewindow.history.fixedTimewindow : null |
||||
|
] |
||||
|
} |
||||
|
), |
||||
|
aggregation: this.fb.group( |
||||
|
{ |
||||
|
type: [ |
||||
|
this.timewindow.aggregation && typeof this.timewindow.aggregation.type !== 'undefined' |
||||
|
? this.timewindow.aggregation.type : null |
||||
|
], |
||||
|
limit: [ |
||||
|
this.timewindow.aggregation && typeof this.timewindow.aggregation.limit !== 'undefined' |
||||
|
? this.timewindow.aggregation.limit : null, |
||||
|
[Validators.min(this.minDatapointsLimit()), Validators.max(this.maxDatapointsLimit())] |
||||
|
] |
||||
|
} |
||||
|
) |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
update() { |
||||
|
const timewindowFormValue = this.timewindowForm.value; |
||||
|
this.timewindow.realtime = new IntervalWindow(); |
||||
|
this.timewindow.realtime.timewindowMs = timewindowFormValue.realtime.timewindowMs; |
||||
|
this.timewindow.realtime.interval = timewindowFormValue.realtime.interval; |
||||
|
this.timewindow.history = new HistoryWindow(); |
||||
|
this.timewindow.history.historyType = timewindowFormValue.history.historyType; |
||||
|
this.timewindow.history.timewindowMs = timewindowFormValue.history.timewindowMs; |
||||
|
this.timewindow.history.interval = timewindowFormValue.history.interval; |
||||
|
this.timewindow.history.fixedTimewindow = timewindowFormValue.history.fixedTimewindow; |
||||
|
if (this.aggregation) { |
||||
|
this.timewindow.aggregation = new Aggregation(); |
||||
|
this.timewindow.aggregation.type = timewindowFormValue.aggregation.type; |
||||
|
this.timewindow.aggregation.limit = timewindowFormValue.aggregation.limit; |
||||
|
} |
||||
|
this.result = this.timewindow; |
||||
|
this.overlayRef.dispose(); |
||||
|
} |
||||
|
|
||||
|
cancel() { |
||||
|
this.overlayRef.dispose(); |
||||
|
} |
||||
|
|
||||
|
minDatapointsLimit() { |
||||
|
return this.timeService.getMinDatapointsLimit(); |
||||
|
} |
||||
|
|
||||
|
maxDatapointsLimit() { |
||||
|
return this.timeService.getMaxDatapointsLimit(); |
||||
|
} |
||||
|
|
||||
|
minRealtimeAggInterval() { |
||||
|
return this.timeService.minIntervalLimit(this.timewindowForm.get('realtime').get('timewindowMs').value); |
||||
|
} |
||||
|
|
||||
|
maxRealtimeAggInterval() { |
||||
|
return this.timeService.maxIntervalLimit(this.timewindowForm.get('realtime').get('timewindowMs').value); |
||||
|
} |
||||
|
|
||||
|
minHistoryAggInterval() { |
||||
|
return this.timeService.minIntervalLimit(this.currentHistoryTimewindow()); |
||||
|
} |
||||
|
|
||||
|
maxHistoryAggInterval() { |
||||
|
return this.timeService.maxIntervalLimit(this.currentHistoryTimewindow()); |
||||
|
} |
||||
|
|
||||
|
currentHistoryTimewindow() { |
||||
|
const timewindowFormValue = this.timewindowForm.value; |
||||
|
if (timewindowFormValue.history.historyType === HistoryWindowType.LAST_INTERVAL) { |
||||
|
return timewindowFormValue.history.timewindowMs; |
||||
|
} else { |
||||
|
return timewindowFormValue.history.fixedTimewindow.endTimeMs - |
||||
|
timewindowFormValue.history.fixedTimewindow.startTimeMs; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,43 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2019 The Thingsboard Authors |
||||
|
|
||||
|
Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
you may not use this file except in compliance with the License. |
||||
|
You may obtain a copy of the License at |
||||
|
|
||||
|
http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
|
||||
|
Unless required by applicable law or agreed to in writing, software |
||||
|
distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
See the License for the specific language governing permissions and |
||||
|
limitations under the License. |
||||
|
|
||||
|
--> |
||||
|
<button *ngIf="asButton" cdkOverlayOrigin #timewindowPanelOrigin="cdkOverlayOrigin" [disabled]="disabled" |
||||
|
mat-raised-button color="primary" (click)="openEditMode($event)"> |
||||
|
<mat-icon class="material-icons">query_builder</mat-icon> |
||||
|
<span>{{innerValue.displayValue}}</span> |
||||
|
</button> |
||||
|
<section *ngIf="!asButton" cdkOverlayOrigin #timewindowPanelOrigin="cdkOverlayOrigin" |
||||
|
class="tb-timewindow" fxLayout="row" fxLayoutAlign="start center"> |
||||
|
<button *ngIf="direction === 'left'" [disabled]="disabled" mat-button mat-icon-button class="tb-mat-32" |
||||
|
(click)="openEditMode($event)" |
||||
|
matTooltip="{{ 'timewindow.edit' | translate }}" |
||||
|
[matTooltipPosition]="tooltipPosition"> |
||||
|
<mat-icon class="material-icons">query_builder</mat-icon> |
||||
|
</button> |
||||
|
<span [fxHide]="hideLabel()" |
||||
|
(click)="openEditMode($event)" |
||||
|
matTooltip="{{ 'timewindow.edit' | translate }}" |
||||
|
[matTooltipPosition]="tooltipPosition"> |
||||
|
{{innerValue.displayValue}} |
||||
|
</span> |
||||
|
<button *ngIf="direction === 'right'" [disabled]="disabled" mat-button mat-icon-button class="tb-mat-32" |
||||
|
(click)="openEditMode($event)" |
||||
|
matTooltip="{{ 'timewindow.edit' | translate }}" |
||||
|
[matTooltipPosition]="tooltipPosition"> |
||||
|
<mat-icon class="material-icons">query_builder</mat-icon> |
||||
|
</button> |
||||
|
</section> |
||||
@ -0,0 +1,26 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2019 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
:host { |
||||
|
section.tb-timewindow { |
||||
|
span { |
||||
|
overflow: hidden; |
||||
|
text-overflow: ellipsis; |
||||
|
white-space: nowrap; |
||||
|
pointer-events: all; |
||||
|
cursor: pointer; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,275 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2019 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { |
||||
|
Component, |
||||
|
forwardRef, Inject, |
||||
|
Input, |
||||
|
OnDestroy, |
||||
|
OnInit, |
||||
|
ViewChild, |
||||
|
ViewContainerRef |
||||
|
} from '@angular/core'; |
||||
|
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; |
||||
|
import { TranslateService } from '@ngx-translate/core'; |
||||
|
import { MillisecondsToTimeStringPipe } from '@shared/pipe/milliseconds-to-time-string.pipe'; |
||||
|
import { |
||||
|
HistoryWindowType, |
||||
|
Timewindow, |
||||
|
TimewindowType |
||||
|
} from '@shared/models/time/time.models'; |
||||
|
import { DatePipe } from '@angular/common'; |
||||
|
import { |
||||
|
Overlay, |
||||
|
CdkOverlayOrigin, |
||||
|
OverlayConfig, |
||||
|
OverlayPositionBuilder, ConnectedPosition, PositionStrategy, OverlayRef |
||||
|
} from '@angular/cdk/overlay'; |
||||
|
import { |
||||
|
TIMEWINDOW_PANEL_DATA, |
||||
|
TimewindowPanelComponent, |
||||
|
TimewindowPanelData |
||||
|
} from '@shared/components/time/timewindow-panel.component'; |
||||
|
import { ComponentPortal, PortalInjector } from '@angular/cdk/portal'; |
||||
|
import { MediaBreakpoints } from '@shared/models/constants'; |
||||
|
import { BreakpointObserver } from '@angular/cdk/layout'; |
||||
|
import { DOCUMENT } from '@angular/common'; |
||||
|
import { WINDOW } from '@core/services/window.service'; |
||||
|
import { TimeService } from '@core/services/time.service'; |
||||
|
import { TooltipPosition } from '@angular/material/typings/tooltip'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-timewindow', |
||||
|
templateUrl: './timewindow.component.html', |
||||
|
styleUrls: ['./timewindow.component.scss'], |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => TimewindowComponent), |
||||
|
multi: true |
||||
|
} |
||||
|
] |
||||
|
}) |
||||
|
export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAccessor { |
||||
|
|
||||
|
historyOnlyValue = false; |
||||
|
|
||||
|
@Input() |
||||
|
set historyOnly(val) { |
||||
|
this.historyOnlyValue = true; |
||||
|
} |
||||
|
|
||||
|
get historyOnly() { |
||||
|
return this.historyOnlyValue; |
||||
|
} |
||||
|
|
||||
|
aggregationValue = false; |
||||
|
|
||||
|
@Input() |
||||
|
set aggregation(val) { |
||||
|
this.aggregationValue = true; |
||||
|
} |
||||
|
|
||||
|
get aggregation() { |
||||
|
return this.aggregationValue; |
||||
|
} |
||||
|
|
||||
|
isToolbarValue = false; |
||||
|
|
||||
|
@Input() |
||||
|
set isToolbar(val) { |
||||
|
this.isToolbarValue = true; |
||||
|
} |
||||
|
|
||||
|
get isToolbar() { |
||||
|
return this.isToolbarValue; |
||||
|
} |
||||
|
|
||||
|
asButtonValue = false; |
||||
|
|
||||
|
@Input() |
||||
|
set asButton(val) { |
||||
|
this.asButtonValue = true; |
||||
|
} |
||||
|
|
||||
|
get asButton() { |
||||
|
return this.asButtonValue; |
||||
|
} |
||||
|
|
||||
|
@Input() |
||||
|
direction: 'left' | 'right' = 'left'; |
||||
|
|
||||
|
@Input() |
||||
|
tooltipPosition: TooltipPosition = 'above'; |
||||
|
|
||||
|
@Input() disabled: boolean; |
||||
|
|
||||
|
@ViewChild('timewindowPanelOrigin', {static: false}) timewindowPanelOrigin: CdkOverlayOrigin; |
||||
|
|
||||
|
innerValue: Timewindow; |
||||
|
|
||||
|
private propagateChange = (_: any) => {}; |
||||
|
|
||||
|
constructor(private translate: TranslateService, |
||||
|
private timeService: TimeService, |
||||
|
private millisecondsToTimeStringPipe: MillisecondsToTimeStringPipe, |
||||
|
private datePipe: DatePipe, |
||||
|
private overlay: Overlay, |
||||
|
public viewContainerRef: ViewContainerRef, |
||||
|
public breakpointObserver: BreakpointObserver, |
||||
|
@Inject(DOCUMENT) private document: Document, |
||||
|
@Inject(WINDOW) private window: Window) { |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
} |
||||
|
|
||||
|
ngOnDestroy(): void { |
||||
|
} |
||||
|
|
||||
|
openEditMode() { |
||||
|
if (this.disabled) { |
||||
|
return; |
||||
|
} |
||||
|
const isGtSm = this.breakpointObserver.isMatched(MediaBreakpoints['gt-sm']); |
||||
|
const position = this.overlay.position(); |
||||
|
const config = new OverlayConfig({ |
||||
|
panelClass: 'tb-timewindow-panel', |
||||
|
backdropClass: 'cdk-overlay-transparent-backdrop', |
||||
|
hasBackdrop: isGtSm, |
||||
|
}); |
||||
|
if (isGtSm) { |
||||
|
config.minWidth = '417px'; |
||||
|
config.maxHeight = '440px'; |
||||
|
const panelHeight = 375; |
||||
|
const panelWidth = 417; |
||||
|
const el = this.timewindowPanelOrigin.elementRef.nativeElement; |
||||
|
const offset = el.getBoundingClientRect(); |
||||
|
const scrollTop = this.window.pageYOffset || this.document.documentElement.scrollTop || this.document.body.scrollTop || 0; |
||||
|
const scrollLeft = this.window.pageXOffset || this.document.documentElement.scrollLeft || this.document.body.scrollLeft || 0; |
||||
|
const bottomY = offset.bottom - scrollTop; |
||||
|
const leftX = offset.left - scrollLeft; |
||||
|
let originX; |
||||
|
let originY; |
||||
|
let overlayX; |
||||
|
let overlayY; |
||||
|
const wHeight = this.document.documentElement.clientHeight; |
||||
|
const wWidth = this.document.documentElement.clientWidth; |
||||
|
if (bottomY + panelHeight > wHeight) { |
||||
|
originY = 'top'; |
||||
|
overlayY = 'bottom'; |
||||
|
} else { |
||||
|
originY = 'bottom'; |
||||
|
overlayY = 'top'; |
||||
|
} |
||||
|
if (leftX + panelWidth > wWidth) { |
||||
|
originX = 'end'; |
||||
|
overlayX = 'end'; |
||||
|
} else { |
||||
|
originX = 'start'; |
||||
|
overlayX = 'start'; |
||||
|
} |
||||
|
const connectedPosition: ConnectedPosition = { |
||||
|
originX, |
||||
|
originY, |
||||
|
overlayX, |
||||
|
overlayY |
||||
|
}; |
||||
|
config.positionStrategy = position.flexibleConnectedTo(this.timewindowPanelOrigin.elementRef) |
||||
|
.withPositions([connectedPosition]); |
||||
|
} else { |
||||
|
config.minWidth = '100%'; |
||||
|
config.minHeight = '100%'; |
||||
|
config.positionStrategy = position.global().top('0%').left('0%') |
||||
|
.right('0%').bottom('0%'); |
||||
|
} |
||||
|
|
||||
|
const overlayRef = this.overlay.create(config); |
||||
|
|
||||
|
overlayRef.backdropClick().subscribe(() => { |
||||
|
overlayRef.dispose(); |
||||
|
}); |
||||
|
|
||||
|
const injector = this._createTimewindowPanelInjector( |
||||
|
overlayRef, |
||||
|
{ |
||||
|
timewindow: this.innerValue.clone(), |
||||
|
historyOnly: this.historyOnly, |
||||
|
aggregation: this.aggregation |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
const componentRef = overlayRef.attach(new ComponentPortal(TimewindowPanelComponent, this.viewContainerRef, injector)); |
||||
|
componentRef.onDestroy(() => { |
||||
|
if (componentRef.instance.result) { |
||||
|
this.innerValue = componentRef.instance.result; |
||||
|
this.updateDisplayValue(); |
||||
|
this.notifyChanged(); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private _createTimewindowPanelInjector(overlayRef: OverlayRef, data: TimewindowPanelData): PortalInjector { |
||||
|
const injectionTokens = new WeakMap<any, any>([ |
||||
|
[TIMEWINDOW_PANEL_DATA, data], |
||||
|
[OverlayRef, overlayRef] |
||||
|
]); |
||||
|
return new PortalInjector(this.viewContainerRef.injector, injectionTokens); |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: any): void { |
||||
|
this.propagateChange = fn; |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(fn: any): void { |
||||
|
} |
||||
|
|
||||
|
setDisabledState(isDisabled: boolean): void { |
||||
|
this.disabled = isDisabled; |
||||
|
} |
||||
|
|
||||
|
writeValue(obj: Timewindow): void { |
||||
|
this.innerValue = Timewindow.initModelFromDefaultTimewindow(obj, this.timeService); |
||||
|
this.updateDisplayValue(); |
||||
|
} |
||||
|
|
||||
|
notifyChanged() { |
||||
|
this.propagateChange(this.innerValue.cloneSelectedTimewindow()); |
||||
|
} |
||||
|
|
||||
|
updateDisplayValue() { |
||||
|
if (this.innerValue.selectedTab === TimewindowType.REALTIME && !this.historyOnly) { |
||||
|
this.innerValue.displayValue = this.translate.instant('timewindow.realtime') + ' - ' + |
||||
|
this.translate.instant('timewindow.last-prefix') + ' ' + |
||||
|
this.millisecondsToTimeStringPipe.transform(this.innerValue.realtime.timewindowMs); |
||||
|
} else { |
||||
|
this.innerValue.displayValue = !this.historyOnly ? (this.translate.instant('timewindow.history') + ' - ') : ''; |
||||
|
if (this.innerValue.history.historyType === HistoryWindowType.LAST_INTERVAL) { |
||||
|
this.innerValue.displayValue += this.translate.instant('timewindow.last-prefix') + ' ' + |
||||
|
this.millisecondsToTimeStringPipe.transform(this.innerValue.history.timewindowMs); |
||||
|
} else { |
||||
|
const startString = this.datePipe.transform(this.innerValue.history.fixedTimewindow.startTimeMs, 'yyyy-MM-dd HH:mm:ss'); |
||||
|
const endString = this.datePipe.transform(this.innerValue.history.fixedTimewindow.endTimeMs, 'yyyy-MM-dd HH:mm:ss'); |
||||
|
this.innerValue.displayValue += this.translate.instant('timewindow.period', {startTime: startString, endTime: endString}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
hideLabel() { |
||||
|
return this.isToolbar && !this.breakpointObserver.isMatched(MediaBreakpoints['gt-md']); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,35 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2019 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import {BaseData} from '@shared/models/base-data'; |
||||
|
import {DashboardId} from '@shared/models/id/dashboard-id'; |
||||
|
import {TenantId} from '@shared/models/id/tenant-id'; |
||||
|
import {ShortCustomerInfo} from '@shared/models/customer.model'; |
||||
|
|
||||
|
export interface DashboardInfo extends BaseData<DashboardId> { |
||||
|
tenantId: TenantId; |
||||
|
title: string; |
||||
|
assignedCustomers: Array<ShortCustomerInfo>; |
||||
|
} |
||||
|
|
||||
|
export interface DashboardConfiguration { |
||||
|
widgets: Array<any>; |
||||
|
// TODO:
|
||||
|
} |
||||
|
|
||||
|
export interface Dashboard extends DashboardInfo { |
||||
|
configuration: DashboardConfiguration; |
||||
|
} |
||||
@ -0,0 +1,113 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2019 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { EntityId } from '@shared/models/id/entity-id'; |
||||
|
import { PageLink } from '@shared/models/page/page-link'; |
||||
|
import { BehaviorSubject, Observable, of, ReplaySubject } from 'rxjs'; |
||||
|
import { emptyPageData, PageData } from '@shared/models/page/page-data'; |
||||
|
import { BaseData, HasId } from '@shared/models/base-data'; |
||||
|
import { CollectionViewer, DataSource } from '@angular/cdk/typings/collections'; |
||||
|
import { catchError, map, take, tap } from 'rxjs/operators'; |
||||
|
import { SelectionModel } from '@angular/cdk/collections'; |
||||
|
|
||||
|
export type EntitiesFetchFunction<T extends BaseData<HasId>, P extends PageLink> = (pageLink: P) => Observable<PageData<T>>; |
||||
|
|
||||
|
export class EntitiesDataSource<T extends BaseData<HasId>, P extends PageLink = PageLink> implements DataSource<T> { |
||||
|
|
||||
|
private entitiesSubject = new BehaviorSubject<T[]>([]); |
||||
|
private pageDataSubject = new BehaviorSubject<PageData<T>>(emptyPageData<T>()); |
||||
|
|
||||
|
public pageData$ = this.pageDataSubject.asObservable(); |
||||
|
|
||||
|
public selection = new SelectionModel<T>(true, []); |
||||
|
|
||||
|
public currentEntity: T = null; |
||||
|
|
||||
|
constructor(private fetchFunction: EntitiesFetchFunction<T, P>) {} |
||||
|
|
||||
|
connect(collectionViewer: CollectionViewer): Observable<T[] | ReadonlyArray<T>> { |
||||
|
return this.entitiesSubject.asObservable(); |
||||
|
} |
||||
|
|
||||
|
disconnect(collectionViewer: CollectionViewer): void { |
||||
|
this.entitiesSubject.complete(); |
||||
|
this.pageDataSubject.complete(); |
||||
|
} |
||||
|
|
||||
|
loadEntities(pageLink: P): Observable<PageData<T>> { |
||||
|
const result = new ReplaySubject<PageData<T>>(); |
||||
|
this.fetchFunction(pageLink).pipe( |
||||
|
tap(() => { |
||||
|
this.selection.clear(); |
||||
|
}), |
||||
|
catchError(() => of(emptyPageData<T>())), |
||||
|
).subscribe( |
||||
|
(pageData) => { |
||||
|
this.entitiesSubject.next(pageData.data); |
||||
|
this.pageDataSubject.next(pageData); |
||||
|
result.next(pageData); |
||||
|
} |
||||
|
); |
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
isAllSelected(): Observable<boolean> { |
||||
|
const numSelected = this.selection.selected.length; |
||||
|
return this.entitiesSubject.pipe( |
||||
|
map((entities) => numSelected === entities.length) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
isEmpty(): Observable<boolean> { |
||||
|
return this.entitiesSubject.pipe( |
||||
|
map((entities) => !entities.length) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
total(): Observable<number> { |
||||
|
return this.pageDataSubject.pipe( |
||||
|
map((pageData) => pageData.totalElements) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
toggleCurrentEntity(entity: T): boolean { |
||||
|
if (this.currentEntity !== entity) { |
||||
|
this.currentEntity = entity; |
||||
|
return true; |
||||
|
} else { |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
isCurrentEntity(entity: T): boolean { |
||||
|
return (this.currentEntity && entity && this.currentEntity.id && entity.id) && |
||||
|
(this.currentEntity.id.id === entity.id.id); |
||||
|
} |
||||
|
|
||||
|
masterToggle() { |
||||
|
this.entitiesSubject.pipe( |
||||
|
tap((entities) => { |
||||
|
const numSelected = this.selection.selected.length; |
||||
|
if (numSelected === entities.length) { |
||||
|
this.selection.clear(); |
||||
|
} else { |
||||
|
entities.forEach(row => this.selection.select(row)); |
||||
|
} |
||||
|
}), |
||||
|
take(1) |
||||
|
).subscribe(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,26 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2019 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { EntityId } from './entity-id'; |
||||
|
import { EntityType } from '@shared/models/entity-type.models'; |
||||
|
|
||||
|
export class DashboardId implements EntityId { |
||||
|
entityType = EntityType.DASHBOARD; |
||||
|
id: string; |
||||
|
constructor(id: string) { |
||||
|
this.id = id; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,27 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2019 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { Pipe, PipeTransform } from '@angular/core'; |
||||
|
|
||||
|
@Pipe({ |
||||
|
name: 'enumToArray' |
||||
|
}) |
||||
|
export class EnumToArrayPipe implements PipeTransform { |
||||
|
transform(data: object) { |
||||
|
const keys = Object.keys(data); |
||||
|
return keys.slice(keys.length / 2); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,28 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2019 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import {Pipe, PipeTransform} from '@angular/core'; |
||||
|
|
||||
|
@Pipe({ name: 'highlight' }) |
||||
|
export class HighlightPipe implements PipeTransform { |
||||
|
transform(text: string, search): string { |
||||
|
const pattern = search |
||||
|
.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'); |
||||
|
const regex = new RegExp('^' + pattern, 'i'); |
||||
|
|
||||
|
return search ? text.replace(regex, match => `<b>${match}</b>`) : text; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,58 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2019 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { Pipe, PipeTransform } from '@angular/core'; |
||||
|
import { TranslateService } from '@ngx-translate/core'; |
||||
|
|
||||
|
@Pipe({ |
||||
|
name: 'milliSecondsToTimeString' |
||||
|
}) |
||||
|
export class MillisecondsToTimeStringPipe implements PipeTransform { |
||||
|
|
||||
|
constructor(private translate: TranslateService) { |
||||
|
} |
||||
|
|
||||
|
transform(millseconds: number, args?: any): string { |
||||
|
let seconds = Math.floor(millseconds / 1000); |
||||
|
const days = Math.floor(seconds / 86400); |
||||
|
let hours = Math.floor((seconds % 86400) / 3600); |
||||
|
let minutes = Math.floor(((seconds % 86400) % 3600) / 60); |
||||
|
seconds = seconds % 60; |
||||
|
let timeString = ''; |
||||
|
if (days > 0) { |
||||
|
timeString += this.translate.instant('timewindow.days', {days}); |
||||
|
} |
||||
|
if (hours > 0) { |
||||
|
if (timeString.length === 0 && hours === 1) { |
||||
|
hours = 0; |
||||
|
} |
||||
|
timeString += this.translate.instant('timewindow.hours', {hours}); |
||||
|
} |
||||
|
if (minutes > 0) { |
||||
|
if (timeString.length === 0 && minutes === 1) { |
||||
|
minutes = 0; |
||||
|
} |
||||
|
timeString += this.translate.instant('timewindow.minutes', {minutes}); |
||||
|
} |
||||
|
if (seconds > 0) { |
||||
|
if (timeString.length === 0 && seconds === 1) { |
||||
|
seconds = 0; |
||||
|
} |
||||
|
timeString += this.translate.instant('timewindow.seconds', {seconds}); |
||||
|
} |
||||
|
return timeString; |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue