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