Commit 08e8c92970519a41d673f38f0bd7ffaa6ff470da

Authored by Igor Kulikov
1 parent 1e7f197c

Tenants and tenant admins pages.

Showing 62 changed files with 4635 additions and 12 deletions

Too many changes to show.

To preserve performance only 62 of 93 files are displayed.

  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Injectable } from '@angular/core';
  18 +import { defaultHttpOptions } from './http-utils';
  19 +import { Observable } from 'rxjs/index';
  20 +import { HttpClient } from '@angular/common/http';
  21 +import {AdminSettings, MailServerSettings, SecuritySettings} from '@shared/models/settings.models';
  22 +
  23 +@Injectable({
  24 + providedIn: 'root'
  25 +})
  26 +export class AdminService {
  27 +
  28 + constructor(
  29 + private http: HttpClient
  30 + ) { }
  31 +
  32 + public getAdminSettings<T>(key: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<AdminSettings<T>> {
  33 + return this.http.get<AdminSettings<T>>(`/api/admin/settings/${key}`, defaultHttpOptions(ignoreLoading, ignoreErrors));
  34 + }
  35 +
  36 + public saveAdminSettings<T>(adminSettings: AdminSettings<T>,
  37 + ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<AdminSettings<T>> {
  38 + return this.http.post<AdminSettings<T>>('/api/admin/settings', adminSettings, defaultHttpOptions(ignoreLoading, ignoreErrors));
  39 + }
  40 +
  41 + public sendTestMail(adminSettings: AdminSettings<MailServerSettings>,
  42 + ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<void> {
  43 + return this.http.post<void>('/api/admin/settings/testMail', adminSettings, defaultHttpOptions(ignoreLoading, ignoreErrors));
  44 + }
  45 +
  46 + public getSecuritySettings(ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<SecuritySettings> {
  47 + return this.http.get<SecuritySettings>(`/api/admin/securitySettings`, defaultHttpOptions(ignoreLoading, ignoreErrors));
  48 + }
  49 +
  50 + public saveSecuritySettings(securitySettings: SecuritySettings,
  51 + ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<SecuritySettings> {
  52 + return this.http.post<SecuritySettings>('/api/admin/securitySettings', securitySettings,
  53 + defaultHttpOptions(ignoreLoading, ignoreErrors));
  54 + }
  55 +}
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Injectable } from '@angular/core';
  18 +import { defaultHttpOptions } from './http-utils';
  19 +import { Observable } from 'rxjs/index';
  20 +import { HttpClient } from '@angular/common/http';
  21 +import { PageLink } from '@shared/models/page/page-link';
  22 +import { PageData } from '@shared/models/page/page-data';
  23 +import { Customer } from '@shared/models/customer.model';
  24 +
  25 +@Injectable({
  26 + providedIn: 'root'
  27 +})
  28 +export class CustomerService {
  29 +
  30 + constructor(
  31 + private http: HttpClient
  32 + ) { }
  33 +
  34 + public getCustomers(tenantId: string, pageLink: PageLink, ignoreErrors: boolean = false,
  35 + ignoreLoading: boolean = false): Observable<PageData<Customer>> {
  36 + return this.http.get<PageData<Customer>>(`/api/tenant/${tenantId}/customers${pageLink.toQuery()}`,
  37 + defaultHttpOptions(ignoreLoading, ignoreErrors));
  38 + }
  39 +
  40 + public getCustomer(customerId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Customer> {
  41 + return this.http.get<Customer>(`/api/customer/${customerId}`, defaultHttpOptions(ignoreLoading, ignoreErrors));
  42 + }
  43 +
  44 + public saveCustomer(customer: Customer, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Customer> {
  45 + return this.http.post<Customer>('/api/customer', customer, defaultHttpOptions(ignoreLoading, ignoreErrors));
  46 + }
  47 +
  48 + public deleteCustomer(customerId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false) {
  49 + return this.http.delete(`/api/customer/${customerId}`, defaultHttpOptions(ignoreLoading, ignoreErrors));
  50 + }
  51 +
  52 +}
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Injectable } from '@angular/core';
  18 +import { defaultHttpOptions } from './http-utils';
  19 +import { Observable } from 'rxjs/index';
  20 +import { HttpClient } from '@angular/common/http';
  21 +import { PageLink } from '@shared/models/page/page-link';
  22 +import { PageData } from '@shared/models/page/page-data';
  23 +import { Tenant } from '@shared/models/tenant.model';
  24 +import {DashboardInfo, Dashboard} from '@shared/models/dashboard.models';
  25 +import {map} from 'rxjs/operators';
  26 +
  27 +@Injectable({
  28 + providedIn: 'root'
  29 +})
  30 +export class DashboardService {
  31 +
  32 + constructor(
  33 + private http: HttpClient
  34 + ) { }
  35 +
  36 + public getTenantDashboards(pageLink: PageLink, ignoreErrors: boolean = false,
  37 + ignoreLoading: boolean = false): Observable<PageData<DashboardInfo>> {
  38 + return this.http.get<PageData<DashboardInfo>>(`/api/tenant/dashboards${pageLink.toQuery()}`,
  39 + defaultHttpOptions(ignoreLoading, ignoreErrors));
  40 + }
  41 +
  42 + public getTenantDashboardsByTenantId(tenantId: string, pageLink: PageLink, ignoreErrors: boolean = false,
  43 + ignoreLoading: boolean = false): Observable<PageData<DashboardInfo>> {
  44 + return this.http.get<PageData<DashboardInfo>>(`/api/tenant/${tenantId}/dashboards${pageLink.toQuery()}`,
  45 + defaultHttpOptions(ignoreLoading, ignoreErrors));
  46 + }
  47 +
  48 + public getCustomerDashboards(customerId: string, pageLink: PageLink, ignoreErrors: boolean = false,
  49 + ignoreLoading: boolean = false): Observable<PageData<DashboardInfo>> {
  50 + return this.http.get<PageData<DashboardInfo>>(`/api/customer/${customerId}/dashboards${pageLink.toQuery()}`,
  51 + defaultHttpOptions(ignoreLoading, ignoreErrors)).pipe(
  52 + map( dashboards => {
  53 + dashboards.data = dashboards.data.filter(dashboard => {
  54 + return dashboard.title.toUpperCase().includes(pageLink.textSearch.toUpperCase());
  55 + });
  56 + return dashboards;
  57 + }
  58 + ));
  59 + }
  60 +
  61 + public getDashboard(dashboardId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Dashboard> {
  62 + return this.http.get<Dashboard>(`/api/dashboard/${dashboardId}`, defaultHttpOptions(ignoreLoading, ignoreErrors));
  63 + }
  64 +
  65 + public getDashboardInfo(dashboardId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<DashboardInfo> {
  66 + return this.http.get<DashboardInfo>(`/api/dashboard/info/${dashboardId}`, defaultHttpOptions(ignoreLoading, ignoreErrors));
  67 + }
  68 +
  69 + public saveDashboard(dashboard: Dashboard, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Dashboard> {
  70 + return this.http.post<Dashboard>('/api/dashboard', dashboard, defaultHttpOptions(ignoreLoading, ignoreErrors));
  71 + }
  72 +
  73 + public deleteDashboard(dashboardId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false) {
  74 + return this.http.delete(`/api/dashboard/${dashboardId}`, defaultHttpOptions(ignoreLoading, ignoreErrors));
  75 + }
  76 +
  77 +}
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Injectable } from '@angular/core';
  18 +import { defaultHttpOptions } from './http-utils';
  19 +import { Observable } from 'rxjs/index';
  20 +import { HttpClient } from '@angular/common/http';
  21 +import { PageLink } from '@shared/models/page/page-link';
  22 +import { PageData } from '@shared/models/page/page-data';
  23 +import { Tenant } from '@shared/models/tenant.model';
  24 +
  25 +@Injectable({
  26 + providedIn: 'root'
  27 +})
  28 +export class TenantService {
  29 +
  30 + constructor(
  31 + private http: HttpClient
  32 + ) { }
  33 +
  34 + public getTenants(pageLink: PageLink, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<PageData<Tenant>> {
  35 + return this.http.get<PageData<Tenant>>(`/api/tenants${pageLink.toQuery()}`, defaultHttpOptions(ignoreLoading, ignoreErrors));
  36 + }
  37 +
  38 + public getTenant(tenantId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Tenant> {
  39 + return this.http.get<Tenant>(`/api/tenant/${tenantId}`, defaultHttpOptions(ignoreLoading, ignoreErrors));
  40 + }
  41 +
  42 + public saveTenant(tenant: Tenant, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Tenant> {
  43 + return this.http.post<Tenant>('/api/tenant', tenant, defaultHttpOptions(ignoreLoading, ignoreErrors));
  44 + }
  45 +
  46 + public deleteTenant(tenantId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false) {
  47 + return this.http.delete(`/api/tenant/${tenantId}`, defaultHttpOptions(ignoreLoading, ignoreErrors));
  48 + }
  49 +
  50 +}
... ...
... ... @@ -320,15 +320,78 @@ export class MenuService {
320 320 type: 'link',
321 321 path: '/home',
322 322 icon: 'home'
  323 + },
  324 + {
  325 + name: 'asset.assets',
  326 + type: 'link',
  327 + path: '/assets',
  328 + icon: 'domain'
  329 + },
  330 + {
  331 + name: 'device.devices',
  332 + type: 'link',
  333 + path: '/devices',
  334 + icon: 'devices_other'
  335 + },
  336 + {
  337 + name: 'entity-view.entity-views',
  338 + type: 'link',
  339 + path: '/entityViews',
  340 + icon: 'view_quilt'
  341 + },
  342 + {
  343 + name: 'dashboard.dashboards',
  344 + type: 'link',
  345 + path: '/dashboards',
  346 + icon: 'dashboard'
323 347 }
324 348 );
325   - // TODO:
326 349 return sections;
327 350 }
328 351
329 352 private buildCustomerUserHome(authUser: any): Array<HomeSection> {
330   - const homeSections: Array<HomeSection> = [];
331   - // TODO:
  353 + const homeSections: Array<HomeSection> = [
  354 + {
  355 + name: 'asset.view-assets',
  356 + places: [
  357 + {
  358 + name: 'asset.assets',
  359 + icon: 'domain',
  360 + path: '/assets'
  361 + }
  362 + ]
  363 + },
  364 + {
  365 + name: 'device.view-devices',
  366 + places: [
  367 + {
  368 + name: 'device.devices',
  369 + icon: 'devices_other',
  370 + path: '/devices'
  371 + }
  372 + ]
  373 + },
  374 + {
  375 + name: 'entity-view.management',
  376 + places: [
  377 + {
  378 + name: 'entity-view.entity-views',
  379 + icon: 'view_quilt',
  380 + path: '/entityViews'
  381 + }
  382 + ]
  383 + },
  384 + {
  385 + name: 'dashboard.view-dashboards',
  386 + places: [
  387 + {
  388 + name: 'dashboard.dashboards',
  389 + icon: 'dashboard',
  390 + path: '/dashboards'
  391 + }
  392 + ]
  393 + }
  394 + ];
332 395 return homeSections;
333 396 }
334 397
... ...
... ... @@ -56,8 +56,14 @@ export class TranslateDefaultCompiler extends TranslateMessageFormatCompiler {
56 56 }
57 57
58 58 private checkIsPlural(src: string): boolean {
59   - const tokens: any[] = parse(src.replace(/\{\{/g, '{').replace(/\}\}/g, '}'),
60   - {cardinal: [], ordinal: []});
  59 + let tokens: any[];
  60 + try {
  61 + tokens = parse(src.replace(/\{\{/g, '{').replace(/\}\}/g, '}'),
  62 + {cardinal: [], ordinal: []});
  63 + } catch (e) {
  64 + console.warn(`Failed to parse source: ${src}`);
  65 + console.error(e);
  66 + }
61 67 const res = tokens.filter(
62 68 (value) => typeof value !== 'string' && value.type === 'plural'
63 69 );
... ...
... ... @@ -106,7 +106,7 @@
106 106 button {
107 107 padding: 0 16px 0 32px;
108 108 font-weight: 500;
109   - text-transform: none;
  109 + text-transform: none !important;
110 110 text-rendering: optimizeLegibility;
111 111 }
112 112 }
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { NgModule } from '@angular/core';
  18 +import { Routes, RouterModule } from '@angular/router';
  19 +
  20 +import { MailServerComponent } from '@modules/home/pages/admin/mail-server.component';
  21 +import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard';
  22 +import { Authority } from '@shared/models/authority.enum';
  23 +import {GeneralSettingsComponent} from "@modules/home/pages/admin/general-settings.component";
  24 +import {SecuritySettingsComponent} from "@modules/home/pages/admin/security-settings.component";
  25 +
  26 +const routes: Routes = [
  27 + {
  28 + path: 'settings',
  29 + data: {
  30 + auth: [Authority.SYS_ADMIN],
  31 + breadcrumb: {
  32 + label: 'admin.system-settings',
  33 + icon: 'settings'
  34 + }
  35 + },
  36 + children: [
  37 + {
  38 + path: '',
  39 + redirectTo: 'general',
  40 + pathMatch: 'full'
  41 + },
  42 + {
  43 + path: 'general',
  44 + component: GeneralSettingsComponent,
  45 + canDeactivate: [ConfirmOnExitGuard],
  46 + data: {
  47 + auth: [Authority.SYS_ADMIN],
  48 + title: 'admin.general-settings',
  49 + breadcrumb: {
  50 + label: 'admin.general',
  51 + icon: 'settings_applications'
  52 + }
  53 + }
  54 + },
  55 + {
  56 + path: 'outgoing-mail',
  57 + component: MailServerComponent,
  58 + canDeactivate: [ConfirmOnExitGuard],
  59 + data: {
  60 + auth: [Authority.SYS_ADMIN],
  61 + title: 'admin.outgoing-mail-settings',
  62 + breadcrumb: {
  63 + label: 'admin.outgoing-mail',
  64 + icon: 'mail'
  65 + }
  66 + }
  67 + },
  68 + {
  69 + path: 'security-settings',
  70 + component: SecuritySettingsComponent,
  71 + canDeactivate: [ConfirmOnExitGuard],
  72 + data: {
  73 + auth: [Authority.SYS_ADMIN],
  74 + title: 'admin.security-settings',
  75 + breadcrumb: {
  76 + label: 'admin.security-settings',
  77 + icon: 'security'
  78 + }
  79 + }
  80 + }
  81 + ]
  82 + }
  83 +];
  84 +
  85 +@NgModule({
  86 + imports: [RouterModule.forChild(routes)],
  87 + exports: [RouterModule]
  88 +})
  89 +export class AdminRoutingModule { }
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { NgModule } from '@angular/core';
  18 +import { CommonModule } from '@angular/common';
  19 +
  20 +import { AdminRoutingModule } from './admin-routing.module';
  21 +import { SharedModule } from '@app/shared/shared.module';
  22 +import { MailServerComponent } from '@modules/home/pages/admin/mail-server.component';
  23 +import {GeneralSettingsComponent} from "@modules/home/pages/admin/general-settings.component";
  24 +import {SecuritySettingsComponent} from "@modules/home/pages/admin/security-settings.component";
  25 +
  26 +@NgModule({
  27 + declarations:
  28 + [
  29 + GeneralSettingsComponent,
  30 + MailServerComponent,
  31 + SecuritySettingsComponent
  32 + ],
  33 + imports: [
  34 + CommonModule,
  35 + SharedModule,
  36 + AdminRoutingModule
  37 + ]
  38 +})
  39 +export class AdminModule { }
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2019 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<div>
  19 + <mat-card class="settings-card">
  20 + <mat-card-title>
  21 + <div fxLayout="row">
  22 + <span class="mat-headline" translate>admin.general-settings</span>
  23 + </div>
  24 + </mat-card-title>
  25 + <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
  26 + </mat-progress-bar>
  27 + <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
  28 + <mat-card-content style="padding-top: 16px;">
  29 + <form #generalSettingsForm="ngForm" [formGroup]="generalSettings" (ngSubmit)="save()">
  30 + <fieldset [disabled]="isLoading$ | async">
  31 + <mat-form-field class="mat-block">
  32 + <mat-label translate>admin.base-url</mat-label>
  33 + <input matInput formControlName="baseUrl" required/>
  34 + <mat-error *ngIf="generalSettings.get('baseUrl').hasError('required')">
  35 + {{ 'admin.base-url-required' | translate }}
  36 + </mat-error>
  37 + </mat-form-field>
  38 + <div fxLayout="row" fxLayoutAlign="end center" style="width: 100%;" class="layout-wrap">
  39 + <button mat-button mat-raised-button color="primary" [disabled]="(isLoading$ | async) || generalSettingsForm.invalid || !generalSettingsForm.dirty"
  40 + type="submit">{{'action.save' | translate}}
  41 + </button>
  42 + </div>
  43 + </fieldset>
  44 + </form>
  45 + </mat-card-content>
  46 + </mat-card>
  47 +</div>
... ...
  1 +/**
  2 + * Copyright © 2016-2019 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +:host {
  17 +
  18 +}
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Component, OnInit } from '@angular/core';
  18 +import { Store } from '@ngrx/store';
  19 +import { AppState } from '@core/core.state';
  20 +import { PageComponent } from '@shared/components/page.component';
  21 +import { Router } from '@angular/router';
  22 +import { FormBuilder, FormGroup, Validators } from '@angular/forms';
  23 +import {AdminSettings, GeneralSettings} from '@shared/models/settings.models';
  24 +import { AdminService } from '@core/http/admin.service';
  25 +import { ActionNotificationShow } from '@core/notification/notification.actions';
  26 +import { TranslateService } from '@ngx-translate/core';
  27 +import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard';
  28 +
  29 +@Component({
  30 + selector: 'tb-general-settings',
  31 + templateUrl: './general-settings.component.html',
  32 + styleUrls: ['./general-settings.component.scss', './settings-card.scss']
  33 +})
  34 +export class GeneralSettingsComponent extends PageComponent implements OnInit, HasConfirmForm {
  35 +
  36 + generalSettings: FormGroup;
  37 + adminSettings: AdminSettings<GeneralSettings>;
  38 +
  39 + constructor(protected store: Store<AppState>,
  40 + private router: Router,
  41 + private adminService: AdminService,
  42 + private translate: TranslateService,
  43 + public fb: FormBuilder) {
  44 + super(store);
  45 + }
  46 +
  47 + ngOnInit() {
  48 + this.buildGeneralServerSettingsForm();
  49 + this.adminService.getAdminSettings<GeneralSettings>('general').subscribe(
  50 + (adminSettings) => {
  51 + this.adminSettings = adminSettings;
  52 + this.generalSettings.reset(this.adminSettings.jsonValue);
  53 + }
  54 + );
  55 + }
  56 +
  57 + buildGeneralServerSettingsForm() {
  58 + this.generalSettings = this.fb.group({
  59 + baseUrl: ['', [Validators.required]]
  60 + });
  61 + }
  62 +
  63 + save(): void {
  64 + this.adminSettings.jsonValue = {...this.adminSettings.jsonValue, ...this.generalSettings.value};
  65 + this.adminService.saveAdminSettings(this.adminSettings).subscribe(
  66 + (adminSettings) => {
  67 + this.adminSettings = adminSettings;
  68 + this.generalSettings.reset(this.adminSettings.jsonValue);
  69 + }
  70 + );
  71 + }
  72 +
  73 + confirmForm(): FormGroup {
  74 + return this.generalSettings;
  75 + }
  76 +
  77 +}
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2019 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<div>
  19 + <mat-card class="settings-card">
  20 + <mat-card-title>
  21 + <div fxLayout="row">
  22 + <span class="mat-headline" translate>admin.outgoing-mail-settings</span>
  23 + <span fxFlex></span>
  24 + <div tb-help="outgoingMailSettings"></div>
  25 + </div>
  26 + </mat-card-title>
  27 + <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
  28 + </mat-progress-bar>
  29 + <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
  30 + <mat-card-content style="padding-top: 16px;">
  31 + <form #mailSettingsForm="ngForm" [formGroup]="mailSettings" (ngSubmit)="save()">
  32 + <fieldset [disabled]="isLoading$ | async">
  33 + <mat-form-field class="mat-block">
  34 + <mat-label translate>admin.mail-from</mat-label>
  35 + <input matInput formControlName="mailFrom" required/>
  36 + <mat-error *ngIf="mailSettings.get('mailFrom').hasError('required')">
  37 + {{ 'admin.mail-from-required' | translate }}
  38 + </mat-error>
  39 + </mat-form-field>
  40 + <mat-form-field class="mat-block">
  41 + <mat-label translate>admin.smtp-protocol</mat-label>
  42 + <mat-select matInput formControlName="smtpProtocol">
  43 + <mat-option *ngFor="let protocol of smtpProtocols" [value]="protocol">
  44 + {{protocol.toUpperCase()}}
  45 + </mat-option>
  46 + </mat-select>
  47 + </mat-form-field>
  48 + <div fxLayout.gt-sm="row" fxLayoutGap.gt-sm="10px">
  49 + <mat-form-field class="mat-block" fxFlex="100" fxFlex.gt-sm="60">
  50 + <mat-label translate>admin.smtp-host</mat-label>
  51 + <input matInput formControlName="smtpHost" placeholder="localhost" required/>
  52 + <mat-error *ngIf="mailSettings.get('smtpHost').hasError('required')">
  53 + {{ 'admin.smtp-host-required' | translate }}
  54 + </mat-error>
  55 + </mat-form-field>
  56 + <mat-form-field class="mat-block" fxFlex="100" fxFlex.gt-sm="40">
  57 + <mat-label translate>admin.smtp-port</mat-label>
  58 + <input matInput #smtpPortInput formControlName="smtpPort" placeholder="25" maxlength="5" required/>
  59 + <mat-hint align="end">{{smtpPortInput.value?.length || 0}}/5</mat-hint>
  60 + <mat-error *ngIf="mailSettings.get('smtpPort').hasError('required')">
  61 + {{ 'admin.smtp-port-required' | translate }}
  62 + </mat-error>
  63 + <mat-error *ngIf="mailSettings.get('smtpPort').hasError('pattern') || mailSettings.get('smtpPort').hasError('maxlength')">
  64 + {{ 'admin.smtp-port-invalid' | translate }}
  65 + </mat-error>
  66 + </mat-form-field>
  67 + </div>
  68 + <mat-form-field class="mat-block">
  69 + <mat-label translate>admin.timeout-msec</mat-label>
  70 + <input matInput #timeoutInput formControlName="timeout" placeholder="10000" maxlength="6" required/>
  71 + <mat-hint align="end">{{timeoutInput.value?.length || 0}}/6</mat-hint>
  72 + <mat-error *ngIf="mailSettings.get('timeout').hasError('required')">
  73 + {{ 'admin.timeout-required' | translate }}
  74 + </mat-error>
  75 + <mat-error *ngIf="mailSettings.get('timeout').hasError('pattern') || mailSettings.get('timeout').hasError('maxlength')">
  76 + {{ 'admin.timeout-invalid' | translate }}
  77 + </mat-error>
  78 + </mat-form-field>
  79 + <tb-checkbox formControlName="enableTls" trueValue="true" falseValue="false">
  80 + {{ 'admin.enable-tls' | translate }}
  81 + </tb-checkbox>
  82 + <mat-form-field class="mat-block">
  83 + <mat-label translate>common.username</mat-label>
  84 + <input matInput formControlName="username" placeholder="{{ 'common.enter-username' | translate }}"/>
  85 + </mat-form-field>
  86 + <mat-form-field class="mat-block">
  87 + <mat-label translate>common.password</mat-label>
  88 + <input matInput formControlName="password" type="password" placeholder="{{ 'common.enter-password' | translate }}"/>
  89 + </mat-form-field>
  90 + <div fxLayout="row" fxLayoutAlign="end center" style="width: 100%;" class="layout-wrap">
  91 + <button mat-button mat-raised-button
  92 + type="button" style="margin-right: 16px;"
  93 + [disabled]="(isLoading$ | async) || mailSettingsForm.invalid" (click)="sendTestMail()">
  94 + {{'admin.send-test-mail' | translate}}
  95 + </button>
  96 + <button mat-button mat-raised-button color="primary" [disabled]="(isLoading$ | async) || mailSettingsForm.invalid || !mailSettingsForm.dirty"
  97 + type="submit">{{'action.save' | translate}}
  98 + </button>
  99 + </div>
  100 + </fieldset>
  101 + </form>
  102 + </mat-card-content>
  103 + </mat-card>
  104 +</div>
... ...
  1 +/**
  2 + * Copyright © 2016-2019 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +:host {
  17 +
  18 +}
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Component, OnInit } from '@angular/core';
  18 +import { Store } from '@ngrx/store';
  19 +import { AppState } from '@core/core.state';
  20 +import { PageComponent } from '@shared/components/page.component';
  21 +import { Router } from '@angular/router';
  22 +import { FormBuilder, FormGroup, Validators } from '@angular/forms';
  23 +import { AdminSettings, MailServerSettings, smtpPortPattern } from '@shared/models/settings.models';
  24 +import { AdminService } from '@core/http/admin.service';
  25 +import { ActionNotificationShow } from '@core/notification/notification.actions';
  26 +import { TranslateService } from '@ngx-translate/core';
  27 +import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard';
  28 +
  29 +@Component({
  30 + selector: 'tb-mail-server',
  31 + templateUrl: './mail-server.component.html',
  32 + styleUrls: ['./mail-server.component.scss', './settings-card.scss']
  33 +})
  34 +export class MailServerComponent extends PageComponent implements OnInit, HasConfirmForm {
  35 +
  36 + mailSettings: FormGroup;
  37 + adminSettings: AdminSettings<MailServerSettings>;
  38 + smtpProtocols = ['smtp', 'smtps'];
  39 +
  40 + constructor(protected store: Store<AppState>,
  41 + private router: Router,
  42 + private adminService: AdminService,
  43 + private translate: TranslateService,
  44 + public fb: FormBuilder) {
  45 + super(store);
  46 + }
  47 +
  48 + ngOnInit() {
  49 + this.buildMailServerSettingsForm();
  50 + this.adminService.getAdminSettings<MailServerSettings>('mail').subscribe(
  51 + (adminSettings) => {
  52 + this.adminSettings = adminSettings;
  53 + this.mailSettings.reset(this.adminSettings.jsonValue);
  54 + }
  55 + );
  56 + }
  57 +
  58 + buildMailServerSettingsForm() {
  59 + this.mailSettings = this.fb.group({
  60 + mailFrom: ['', [Validators.required]],
  61 + smtpProtocol: ['smtp'],
  62 + smtpHost: ['localhost', [Validators.required]],
  63 + smtpPort: ['25', [Validators.required,
  64 + Validators.pattern(smtpPortPattern),
  65 + Validators.maxLength(5)]],
  66 + timeout: ['10000', [Validators.required,
  67 + Validators.pattern(/^[0-9]{1,6}$/),
  68 + Validators.maxLength(6)]],
  69 + enableTls: ['false'],
  70 + username: [''],
  71 + password: ['']
  72 + });
  73 + this.registerDisableOnLoadFormControl(this.mailSettings.get('smtpProtocol'));
  74 + this.registerDisableOnLoadFormControl(this.mailSettings.get('enableTls'));
  75 + }
  76 +
  77 + sendTestMail(): void {
  78 + this.adminSettings.jsonValue = {...this.adminSettings.jsonValue, ...this.mailSettings.value};
  79 + this.adminService.sendTestMail(this.adminSettings).subscribe(
  80 + () => {
  81 + this.store.dispatch(new ActionNotificationShow({ message: this.translate.instant('admin.test-mail-sent'),
  82 + type: 'success' }));
  83 + }
  84 + );
  85 + }
  86 +
  87 + save(): void {
  88 + this.adminSettings.jsonValue = {...this.adminSettings.jsonValue, ...this.mailSettings.value};
  89 + this.adminService.saveAdminSettings(this.adminSettings).subscribe(
  90 + (adminSettings) => {
  91 + this.adminSettings = adminSettings;
  92 + this.mailSettings.reset(this.adminSettings.jsonValue);
  93 + }
  94 + );
  95 + }
  96 +
  97 + confirmForm(): FormGroup {
  98 + return this.mailSettings;
  99 + }
  100 +
  101 +}
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2019 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<div>
  19 + <mat-card class="settings-card">
  20 + <mat-card-title>
  21 + <div fxLayout="row">
  22 + <span class="mat-headline" translate>admin.security-settings</span>
  23 + <span fxFlex></span>
  24 + <div tb-help="securitySettings"></div>
  25 + </div>
  26 + </mat-card-title>
  27 + <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
  28 + </mat-progress-bar>
  29 + <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
  30 + <mat-card-content style="padding-top: 16px;">
  31 + <form #securitySettingsForm="ngForm" [formGroup]="securitySettingsFormGroup" (ngSubmit)="save()">
  32 + <fieldset [disabled]="isLoading$ | async">
  33 + <mat-expansion-panel [expanded]="true">
  34 + <mat-expansion-panel-header>
  35 + <mat-panel-title>
  36 + <div class="tb-panel-title" translate>admin.password-policy</div>
  37 + </mat-panel-title>
  38 + </mat-expansion-panel-header>
  39 + <section formGroupName="passwordPolicy">
  40 + <mat-form-field class="mat-block">
  41 + <mat-label translate>admin.minimum-password-length</mat-label>
  42 + <input matInput type="number"
  43 + formControlName="minimumLength"
  44 + step="1"
  45 + min="5"
  46 + max="50"
  47 + required/>
  48 + <mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy').get('minimumLength').hasError('required')">
  49 + {{ 'admin.minimum-password-length-required' | translate }}
  50 + </mat-error>
  51 + <mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy').get('minimumLength').hasError('min')">
  52 + {{ 'admin.minimum-password-length-range' | translate }}
  53 + </mat-error>
  54 + <mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy').get('minimumLength').hasError('max')">
  55 + {{ 'admin.minimum-password-length-range' | translate }}
  56 + </mat-error>
  57 + </mat-form-field>
  58 + <mat-form-field class="mat-block">
  59 + <mat-label translate>admin.minimum-uppercase-letters</mat-label>
  60 + <input matInput type="number"
  61 + formControlName="minimumUppercaseLetters"
  62 + step="1"
  63 + min="0"/>
  64 + <mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy').get('minimumUppercaseLetters').hasError('min')">
  65 + {{ 'admin.minimum-uppercase-letters-range' | translate }}
  66 + </mat-error>
  67 + </mat-form-field>
  68 + <mat-form-field class="mat-block">
  69 + <mat-label translate>admin.minimum-lowercase-letters</mat-label>
  70 + <input matInput type="number"
  71 + formControlName="minimumLowercaseLetters"
  72 + step="1"
  73 + min="0"/>
  74 + <mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy').get('minimumLowercaseLetters').hasError('min')">
  75 + {{ 'admin.minimum-lowercase-letters-range' | translate }}
  76 + </mat-error>
  77 + </mat-form-field>
  78 + <mat-form-field class="mat-block">
  79 + <mat-label translate>admin.minimum-digits</mat-label>
  80 + <input matInput type="number"
  81 + formControlName="minimumDigits"
  82 + step="1"
  83 + min="0"/>
  84 + <mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy').get('minimumDigits').hasError('min')">
  85 + {{ 'admin.minimum-digits-range' | translate }}
  86 + </mat-error>
  87 + </mat-form-field>
  88 + <mat-form-field class="mat-block">
  89 + <mat-label translate>admin.minimum-special-characters</mat-label>
  90 + <input matInput type="number"
  91 + formControlName="minimumSpecialCharacters"
  92 + step="1"
  93 + min="0"/>
  94 + <mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy').get('minimumSpecialCharacters').hasError('min')">
  95 + {{ 'admin.minimum-special-characters-range' | translate }}
  96 + </mat-error>
  97 + </mat-form-field>
  98 + <mat-form-field class="mat-block">
  99 + <mat-label translate>admin.password-expiration-period-days</mat-label>
  100 + <input matInput type="number"
  101 + formControlName="passwordExpirationPeriodDays"
  102 + step="1"
  103 + min="0"/>
  104 + <mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy').get('passwordExpirationPeriodDays').hasError('min')">
  105 + {{ 'admin.password-expiration-period-days-range' | translate }}
  106 + </mat-error>
  107 + </mat-form-field>
  108 + </section>
  109 + </mat-expansion-panel>
  110 + <div fxLayout="row" fxLayoutAlign="end center" style="width: 100%;" class="layout-wrap">
  111 + <button mat-button mat-raised-button color="primary" [disabled]="(isLoading$ | async) || securitySettingsForm.invalid || !securitySettingsForm.dirty"
  112 + type="submit">{{'action.save' | translate}}
  113 + </button>
  114 + </div>
  115 + </fieldset>
  116 + </form>
  117 + </mat-card-content>
  118 + </mat-card>
  119 +</div>
... ...
  1 +/**
  2 + * Copyright © 2016-2019 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +:host {
  17 + mat-expansion-panel {
  18 + margin-bottom: 16px;
  19 + }
  20 + .tb-panel-title {
  21 +
  22 + }
  23 +}
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Component, OnInit } from '@angular/core';
  18 +import { Store } from '@ngrx/store';
  19 +import { AppState } from '@core/core.state';
  20 +import { PageComponent } from '@shared/components/page.component';
  21 +import { Router } from '@angular/router';
  22 +import { FormBuilder, FormGroup, Validators } from '@angular/forms';
  23 +import { SecuritySettings} from '@shared/models/settings.models';
  24 +import { AdminService } from '@core/http/admin.service';
  25 +import { ActionNotificationShow } from '@core/notification/notification.actions';
  26 +import { TranslateService } from '@ngx-translate/core';
  27 +import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard';
  28 +
  29 +@Component({
  30 + selector: 'tb-security-settings',
  31 + templateUrl: './security-settings.component.html',
  32 + styleUrls: ['./security-settings.component.scss', './settings-card.scss']
  33 +})
  34 +export class SecuritySettingsComponent extends PageComponent implements OnInit, HasConfirmForm {
  35 +
  36 + securitySettingsFormGroup: FormGroup;
  37 + securitySettings: SecuritySettings;
  38 +
  39 + constructor(protected store: Store<AppState>,
  40 + private router: Router,
  41 + private adminService: AdminService,
  42 + private translate: TranslateService,
  43 + public fb: FormBuilder) {
  44 + super(store);
  45 + }
  46 +
  47 + ngOnInit() {
  48 + this.buildSecuritySettingsForm();
  49 + this.adminService.getSecuritySettings().subscribe(
  50 + (securitySettings) => {
  51 + this.securitySettings = securitySettings;
  52 + this.securitySettingsFormGroup.reset(this.securitySettings);
  53 + }
  54 + );
  55 + }
  56 +
  57 + buildSecuritySettingsForm() {
  58 + this.securitySettingsFormGroup = this.fb.group({
  59 + passwordPolicy: this.fb.group(
  60 + {
  61 + minimumLength: [null, [Validators.required, Validators.min(5), Validators.max(50)]],
  62 + minimumUppercaseLetters: [null, Validators.min(0)],
  63 + minimumLowercaseLetters: [null, Validators.min(0)],
  64 + minimumDigits: [null, Validators.min(0)],
  65 + minimumSpecialCharacters: [null, Validators.min(0)],
  66 + passwordExpirationPeriodDays: [null, Validators.min(0)]
  67 + }
  68 + )
  69 + });
  70 + }
  71 +
  72 + save(): void {
  73 + this.securitySettings = {...this.securitySettings, ...this.securitySettingsFormGroup.value};
  74 + this.adminService.saveSecuritySettings(this.securitySettings).subscribe(
  75 + (securitySettings) => {
  76 + this.securitySettings = securitySettings;
  77 + this.securitySettingsFormGroup.reset(this.securitySettings);
  78 + }
  79 + );
  80 + }
  81 +
  82 + confirmForm(): FormGroup {
  83 + return this.securitySettingsFormGroup;
  84 + }
  85 +
  86 +}
... ...
  1 +/**
  2 + * Copyright © 2016-2019 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +@import "../../../../../scss/constants";
  17 +
  18 +:host {
  19 + mat-card.settings-card {
  20 + margin: 8px;
  21 + @media #{$mat-gt-sm} {
  22 + width: 60%;
  23 + }
  24 + }
  25 +}
... ...
... ... @@ -16,21 +16,23 @@
16 16
17 17 import { NgModule } from '@angular/core';
18 18
19   -// import { AdminModule } from './admin/admin.module';
  19 +import { AdminModule } from './admin/admin.module';
20 20 import { HomeLinksModule } from './home-links/home-links.module';
21   -// import { ProfileModule } from './profile/profile.module';
  21 +import { ProfileModule } from './profile/profile.module';
  22 +import { TenantModule } from '@modules/home/pages/tenant/tenant.module';
22 23 // import { CustomerModule } from '@modules/home/pages/customer/customer.module';
23 24 // import { AuditLogModule } from '@modules/home/pages/audit-log/audit-log.module';
24   -// import { UserModule } from '@modules/home/pages/user/user.module';
  25 +import { UserModule } from '@modules/home/pages/user/user.module';
25 26
26 27 @NgModule({
27 28 exports: [
28   -// AdminModule,
  29 + AdminModule,
29 30 HomeLinksModule,
30   -// ProfileModule,
  31 + ProfileModule,
  32 + TenantModule,
31 33 // CustomerModule,
32 34 // AuditLogModule,
33   -// UserModule
  35 + UserModule
34 36 ]
35 37 })
36 38 export class HomePagesModule { }
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2019 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<form #changePasswordForm="ngForm" [formGroup]="changePassword" (ngSubmit)="onChangePassword()">
  19 + <mat-toolbar fxLayout="row" color="primary">
  20 + <h2 translate>profile.change-password</h2>
  21 + <span fxFlex></span>
  22 + <button mat-button mat-icon-button
  23 + [mat-dialog-close]="false"
  24 + type="button">
  25 + <mat-icon class="material-icons">close</mat-icon>
  26 + </button>
  27 + </mat-toolbar>
  28 + <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
  29 + </mat-progress-bar>
  30 + <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
  31 + <div mat-dialog-content>
  32 + <mat-form-field class="mat-block">
  33 + <mat-label translate>profile.current-password</mat-label>
  34 + <input matInput type="password" formControlName="currentPassword"/>
  35 + <mat-icon class="material-icons" matPrefix>lock</mat-icon>
  36 + </mat-form-field>
  37 + <mat-form-field class="mat-block">
  38 + <mat-label translate>login.new-password</mat-label>
  39 + <input matInput type="password" formControlName="newPassword"/>
  40 + <mat-icon class="material-icons" matPrefix>lock</mat-icon>
  41 + </mat-form-field>
  42 + <mat-form-field class="mat-block">
  43 + <mat-label translate>login.new-password-again</mat-label>
  44 + <input matInput type="password" formControlName="newPassword2"/>
  45 + <mat-icon class="material-icons" matPrefix>lock</mat-icon>
  46 + </mat-form-field>
  47 + </div>
  48 + <div mat-dialog-actions fxLayout="row">
  49 + <span fxFlex></span>
  50 + <button mat-button mat-raised-button color="primary"
  51 + type="submit"
  52 + [disabled]="(isLoading$ | async) || changePasswordForm.invalid">
  53 + {{ 'profile.change-password' | translate }}
  54 + </button>
  55 + <button mat-button color="primary"
  56 + style="margin-right: 20px;"
  57 + type="button"
  58 + [disabled]="(isLoading$ | async)"
  59 + [mat-dialog-close]="false" cdkFocusInitial>
  60 + {{ 'action.cancel' | translate }}
  61 + </button>
  62 + </div>
  63 +</form>
... ...
  1 +/**
  2 + * Copyright © 2016-2019 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +:host {
  17 +}
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Component, OnInit } from '@angular/core';
  18 +import { MatDialogRef } from '@angular/material';
  19 +import { PageComponent } from '@shared/components/page.component';
  20 +import { Store } from '@ngrx/store';
  21 +import { AppState } from '@core/core.state';
  22 +import { FormBuilder, FormGroup, Validators } from '@angular/forms';
  23 +import { ActionNotificationShow } from '@core/notification/notification.actions';
  24 +import { TranslateService } from '@ngx-translate/core';
  25 +import { AuthService } from '@core/auth/auth.service';
  26 +
  27 +@Component({
  28 + selector: 'tb-change-password-dialog',
  29 + templateUrl: './change-password-dialog.component.html',
  30 + styleUrls: ['./change-password-dialog.component.scss']
  31 +})
  32 +export class ChangePasswordDialogComponent extends PageComponent implements OnInit {
  33 +
  34 + changePassword: FormGroup;
  35 +
  36 + constructor(protected store: Store<AppState>,
  37 + private translate: TranslateService,
  38 + private authService: AuthService,
  39 + public dialogRef: MatDialogRef<ChangePasswordDialogComponent>,
  40 + public fb: FormBuilder) {
  41 + super(store);
  42 + }
  43 +
  44 + ngOnInit(): void {
  45 + this.buildChangePasswordForm();
  46 + }
  47 +
  48 + buildChangePasswordForm() {
  49 + this.changePassword = this.fb.group({
  50 + currentPassword: [''],
  51 + newPassword: [''],
  52 + newPassword2: ['']
  53 + });
  54 + }
  55 +
  56 + onChangePassword(): void {
  57 + if (this.changePassword.get('newPassword').value !== this.changePassword.get('newPassword2').value) {
  58 + this.store.dispatch(new ActionNotificationShow({ message: this.translate.instant('login.passwords-mismatch-error'),
  59 + type: 'error' }));
  60 + } else {
  61 + this.authService.changePassword(
  62 + this.changePassword.get('currentPassword').value,
  63 + this.changePassword.get('newPassword').value).subscribe(() => {
  64 + this.dialogRef.close(true);
  65 + });
  66 + }
  67 + }
  68 +}
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import {Injectable, NgModule} from '@angular/core';
  18 +import {Resolve, RouterModule, Routes} from '@angular/router';
  19 +
  20 +import {ProfileComponent} from './profile.component';
  21 +import {ConfirmOnExitGuard} from '@core/guards/confirm-on-exit.guard';
  22 +import {Authority} from '@shared/models/authority.enum';
  23 +import {User} from '@shared/models/user.model';
  24 +import {Store} from '@ngrx/store';
  25 +import {AppState} from '@core/core.state';
  26 +import {UserService} from '@core/http/user.service';
  27 +import {getCurrentAuthUser} from '@core/auth/auth.selectors';
  28 +import {Observable} from 'rxjs';
  29 +
  30 +@Injectable()
  31 +export class UserProfileResolver implements Resolve<User> {
  32 +
  33 + constructor(private store: Store<AppState>,
  34 + private userService: UserService) {
  35 + }
  36 +
  37 + resolve(): Observable<User> {
  38 + const userId = getCurrentAuthUser(this.store).userId;
  39 + return this.userService.getUser(userId);
  40 + }
  41 +}
  42 +
  43 +const routes: Routes = [
  44 + {
  45 + path: 'profile',
  46 + component: ProfileComponent,
  47 + canDeactivate: [ConfirmOnExitGuard],
  48 + data: {
  49 + auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN, Authority.CUSTOMER_USER],
  50 + title: 'profile.profile',
  51 + breadcrumb: {
  52 + label: 'profile.profile',
  53 + icon: 'account_circle'
  54 + }
  55 + },
  56 + resolve: {
  57 + user: UserProfileResolver
  58 + }
  59 + }
  60 +];
  61 +
  62 +@NgModule({
  63 + imports: [RouterModule.forChild(routes)],
  64 + exports: [RouterModule],
  65 + providers: [
  66 + UserProfileResolver
  67 + ]
  68 +})
  69 +export class ProfileRoutingModule { }
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2019 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<div>
  19 + <mat-card class="profile-card">
  20 + <mat-card-title>
  21 + <div fxLayout="column">
  22 + <span class="mat-headline" translate>profile.profile</span>
  23 + <span class="profile-email" style='opacity: 0.7;'>{{ profile ? profile.get('email').value : '' }}</span>
  24 + </div>
  25 + </mat-card-title>
  26 + <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
  27 + </mat-progress-bar>
  28 + <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
  29 + <mat-card-content style="padding-top: 16px;">
  30 + <form #profileForm="ngForm" [formGroup]="profile" (ngSubmit)="save()">
  31 + <fieldset [disabled]="isLoading$ | async">
  32 + <mat-form-field class="mat-block">
  33 + <mat-label translate>user.email</mat-label>
  34 + <input matInput formControlName="email" required/>
  35 + <mat-error *ngIf="profile.get('email').hasError('required')">
  36 + {{ 'user.email-required' | translate }}
  37 + </mat-error>
  38 + <mat-error *ngIf="profile.get('email').hasError('email')">
  39 + {{ 'user.invalid-email-format' | translate }}
  40 + </mat-error>
  41 + </mat-form-field>
  42 + <mat-form-field class="mat-block">
  43 + <mat-label translate>user.first-name</mat-label>
  44 + <input matInput formControlName="firstName"/>
  45 + </mat-form-field>
  46 + <mat-form-field class="mat-block">
  47 + <mat-label translate>user.last-name</mat-label>
  48 + <input matInput formControlName="lastName"/>
  49 + </mat-form-field>
  50 + <mat-form-field class="mat-block">
  51 + <mat-label translate>language.language</mat-label>
  52 + <mat-select matInput formControlName="language">
  53 + <mat-option *ngFor="let lang of languageList" [value]="lang">
  54 + {{ lang ? ('language.locales.' + lang | translate) : ''}}
  55 + </mat-option>
  56 + </mat-select>
  57 + </mat-form-field>
  58 + <div fxLayout="row" style="padding-bottom: 16px;">
  59 + <button mat-button mat-raised-button color="primary"
  60 + type="button"
  61 + [disabled]="(isLoading$ | async)" (click)="changePassword()">
  62 + {{'profile.change-password' | translate}}
  63 + </button>
  64 + </div>
  65 + <div fxLayout="row" class="layout-wrap">
  66 + <span fxFlex></span>
  67 + <button mat-button mat-raised-button color="primary"
  68 + type="submit"
  69 + [disabled]="(isLoading$ | async) || profileForm.invalid || !profileForm.dirty">
  70 + {{ 'action.save' | translate }}
  71 + </button>
  72 + </div>
  73 + </fieldset>
  74 + </form>
  75 + </mat-card-content>
  76 + </mat-card>
  77 +</div>
... ...
  1 +/**
  2 + * Copyright © 2016-2019 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +@import "../../../../../scss/constants";
  17 +
  18 +:host {
  19 + mat-card.profile-card {
  20 + margin: 8px;
  21 + @media #{$mat-gt-sm} {
  22 + width: 60%;
  23 + }
  24 + .mat-headline {
  25 + margin: 0;
  26 + }
  27 + .profile-email {
  28 + font-size: 16px;
  29 + font-weight: 400;
  30 + }
  31 + }
  32 +}
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Component, OnInit } from '@angular/core';
  18 +import { UserService } from '@core/http/user.service';
  19 +import { User } from '@shared/models/user.model';
  20 +import { Authority } from '@shared/models/authority.enum';
  21 +import { PageComponent } from '@shared/components/page.component';
  22 +import { select, Store } from '@ngrx/store';
  23 +import { AppState } from '@core/core.state';
  24 +import { getCurrentAuthUser, selectAuthUser } from '@core/auth/auth.selectors';
  25 +import { mergeMap, take } from 'rxjs/operators';
  26 +import { FormBuilder, FormGroup, Validators } from '@angular/forms';
  27 +import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard';
  28 +import { ActionAuthUpdateUserDetails } from '@core/auth/auth.actions';
  29 +import { environment as env } from '@env/environment';
  30 +import { TranslateService } from '@ngx-translate/core';
  31 +import { ActionSettingsChangeLanguage } from '@core/settings/settings.actions';
  32 +import { ChangePasswordDialogComponent } from '@modules/home/pages/profile/change-password-dialog.component';
  33 +import { MatDialog } from '@angular/material';
  34 +import { DialogService } from '@core/services/dialog.service';
  35 +import { AuthService } from '@core/auth/auth.service';
  36 +import { ActivatedRoute } from '@angular/router';
  37 +
  38 +@Component({
  39 + selector: 'tb-profile',
  40 + templateUrl: './profile.component.html',
  41 + styleUrls: ['./profile.component.scss']
  42 +})
  43 +export class ProfileComponent extends PageComponent implements OnInit, HasConfirmForm {
  44 +
  45 + authorities = Authority;
  46 + profile: FormGroup;
  47 + user: User;
  48 + languageList = env.supportedLangs;
  49 +
  50 + constructor(protected store: Store<AppState>,
  51 + private route: ActivatedRoute,
  52 + private userService: UserService,
  53 + private authService: AuthService,
  54 + private translate: TranslateService,
  55 + public dialog: MatDialog,
  56 + public dialogService: DialogService,
  57 + public fb: FormBuilder) {
  58 + super(store);
  59 + }
  60 +
  61 + ngOnInit() {
  62 + this.buildProfileForm();
  63 + this.userLoaded(this.route.snapshot.data.user);
  64 + }
  65 +
  66 + buildProfileForm() {
  67 + this.profile = this.fb.group({
  68 + email: ['', [Validators.required, Validators.email]],
  69 + firstName: [''],
  70 + lastName: [''],
  71 + language: ['']
  72 + });
  73 + }
  74 +
  75 + save(): void {
  76 + this.user = {...this.user, ...this.profile.value};
  77 + if (!this.user.additionalInfo) {
  78 + this.user.additionalInfo = {};
  79 + }
  80 + this.user.additionalInfo.lang = this.profile.get('language').value;
  81 + this.userService.saveUser(this.user).subscribe(
  82 + (user) => {
  83 + this.userLoaded(user);
  84 + this.store.dispatch(new ActionAuthUpdateUserDetails({ userDetails: {
  85 + additionalInfo: {...user.additionalInfo},
  86 + authority: user.authority,
  87 + createdTime: user.createdTime,
  88 + tenantId: user.tenantId,
  89 + customerId: user.customerId,
  90 + email: user.email,
  91 + firstName: user.firstName,
  92 + id: user.id,
  93 + lastName: user.lastName,
  94 + } }));
  95 + this.store.dispatch(new ActionSettingsChangeLanguage({ userLang: user.additionalInfo.lang }));
  96 + }
  97 + );
  98 + }
  99 +
  100 + changePassword(): void {
  101 + this.dialog.open(ChangePasswordDialogComponent, {
  102 + disableClose: true,
  103 + panelClass: ['tb-dialog', 'tb-fullscreen-dialog']
  104 + });
  105 + }
  106 +
  107 + userLoaded(user: User) {
  108 + this.user = user;
  109 + this.profile.reset(user);
  110 + let lang;
  111 + if (user.additionalInfo && user.additionalInfo.lang) {
  112 + lang = user.additionalInfo.lang;
  113 + } else {
  114 + lang = this.translate.currentLang;
  115 + }
  116 + this.profile.get('language').setValue(lang);
  117 + }
  118 +
  119 + confirmForm(): FormGroup {
  120 + return this.profile;
  121 + }
  122 +
  123 +}
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { NgModule } from '@angular/core';
  18 +import { CommonModule } from '@angular/common';
  19 +import { ProfileComponent } from './profile.component';
  20 +import { SharedModule } from '@shared/shared.module';
  21 +import { ProfileRoutingModule } from './profile-routing.module';
  22 +import { ChangePasswordDialogComponent } from '@modules/home/pages/profile/change-password-dialog.component';
  23 +
  24 +@NgModule({
  25 + entryComponents: [
  26 + ChangePasswordDialogComponent
  27 + ],
  28 + declarations: [
  29 + ProfileComponent,
  30 + ChangePasswordDialogComponent
  31 + ],
  32 + imports: [
  33 + CommonModule,
  34 + SharedModule,
  35 + ProfileRoutingModule
  36 + ]
  37 +})
  38 +export class ProfileModule { }
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Injectable, NgModule } from '@angular/core';
  18 +import { Resolve, RouterModule, Routes } from '@angular/router';
  19 +
  20 +import { EntitiesTableComponent } from '@shared/components/entity/entities-table.component';
  21 +import { Authority } from '@shared/models/authority.enum';
  22 +import { TenantsTableConfigResolver } from '@modules/home/pages/tenant/tenants-table-config.resolver';
  23 +import { ProfileComponent } from '@modules/home/pages/profile/profile.component';
  24 +import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard';
  25 +import { Customer } from '@shared/models/customer.model';
  26 +import { Store } from '@ngrx/store';
  27 +import { AppState } from '@core/core.state';
  28 +import { forkJoin, Observable, throwError } from 'rxjs';
  29 +import { getCurrentAuthUser } from '@core/auth/auth.selectors';
  30 +import { catchError, finalize, map, tap } from 'rxjs/operators';
  31 +import {UsersTableConfigResolver} from '../user/users-table-config.resolver';
  32 +
  33 +const routes: Routes = [
  34 + {
  35 + path: 'tenants',
  36 + data: {
  37 + breadcrumb: {
  38 + label: 'tenant.tenants',
  39 + icon: 'supervisor_account'
  40 + }
  41 + },
  42 + children: [
  43 + {
  44 + path: '',
  45 + component: EntitiesTableComponent,
  46 + data: {
  47 + auth: [Authority.SYS_ADMIN],
  48 + title: 'tenant.tenants'
  49 + },
  50 + resolve: {
  51 + entitiesTableConfig: TenantsTableConfigResolver
  52 + }
  53 + },
  54 + {
  55 + path: ':tenantId/users',
  56 + component: EntitiesTableComponent,
  57 + data: {
  58 + auth: [Authority.SYS_ADMIN],
  59 + title: 'user.tenant-admins',
  60 + breadcrumb: {
  61 + label: 'user.tenant-admins',
  62 + icon: 'account_circle'
  63 + }
  64 + },
  65 + resolve: {
  66 + entitiesTableConfig: UsersTableConfigResolver
  67 + }
  68 + }
  69 + ]
  70 + }
  71 +];
  72 +
  73 +@NgModule({
  74 + imports: [RouterModule.forChild(routes)],
  75 + exports: [RouterModule],
  76 + providers: [
  77 + TenantsTableConfigResolver
  78 + ]
  79 +})
  80 +export class TenantRoutingModule { }
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2019 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<div class="tb-details-buttons">
  19 + <button mat-raised-button color="primary"
  20 + [disabled]="(isLoading$ | async)"
  21 + (click)="onEntityAction($event, 'manageTenantAdmins')"
  22 + [fxShow]="!isEdit">
  23 + {{'tenant.manage-tenant-admins' | translate }}
  24 + </button>
  25 + <button mat-raised-button color="primary"
  26 + [disabled]="(isLoading$ | async)"
  27 + (click)="onEntityAction($event, 'delete')"
  28 + [fxShow]="!hideDelete() && !isEdit">
  29 + {{'tenant.delete' | translate }}
  30 + </button>
  31 + <div fxLayout="row">
  32 + <button mat-raised-button
  33 + ngxClipboard
  34 + (cbOnSuccess)="onTenantIdCopied($event)"
  35 + [cbContent]="entity?.id?.id"
  36 + [fxShow]="!isEdit">
  37 + <mat-icon svgIcon="mdi:clipboard-arrow-left"></mat-icon>
  38 + <span translate>tenant.copyId</span>
  39 + </button>
  40 + </div>
  41 +</div>
  42 +<div class="mat-padding" fxLayout="column">
  43 + <form #entityNgForm="ngForm" [formGroup]="entityForm">
  44 + <fieldset [disabled]="(isLoading$ | async) || !isEdit">
  45 + <mat-form-field class="mat-block">
  46 + <mat-label translate>tenant.title</mat-label>
  47 + <input matInput formControlName="title" required/>
  48 + <mat-error *ngIf="entityForm.get('title').hasError('required')">
  49 + {{ 'tenant.title-required' | translate }}
  50 + </mat-error>
  51 + </mat-form-field>
  52 + <div formGroupName="additionalInfo" fxLayout="column">
  53 + <mat-form-field class="mat-block">
  54 + <mat-label translate>tenant.description</mat-label>
  55 + <textarea matInput formControlName="description" rows="2"></textarea>
  56 + </mat-form-field>
  57 + </div>
  58 + <tb-contact [parentForm]="entityForm" [isEdit]="isEdit"></tb-contact>
  59 + </fieldset>
  60 + </form>
  61 +</div>
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Component } from '@angular/core';
  18 +import { Store } from '@ngrx/store';
  19 +import { AppState } from '@core/core.state';
  20 +import { FormBuilder, FormGroup, Validators } from '@angular/forms';
  21 +import { Customer } from '@shared/models/customer.model';
  22 +import { ContactBasedComponent } from '@shared/components/entity/contact-based.component';
  23 +import {Tenant} from '@app/shared/models/tenant.model';
  24 +import {ActionNotificationShow} from '@app/core/notification/notification.actions';
  25 +import {TranslateService} from '@ngx-translate/core';
  26 +
  27 +@Component({
  28 + selector: 'tb-tenant',
  29 + templateUrl: './tenant.component.html'
  30 +})
  31 +export class TenantComponent extends ContactBasedComponent<Tenant> {
  32 +
  33 + constructor(protected store: Store<AppState>,
  34 + protected translate: TranslateService,
  35 + protected fb: FormBuilder) {
  36 + super(store, fb);
  37 + }
  38 +
  39 + hideDelete() {
  40 + if (this.entitiesTableConfig) {
  41 + return !this.entitiesTableConfig.deleteEnabled(this.entity);
  42 + } else {
  43 + return false;
  44 + }
  45 + }
  46 +
  47 + buildEntityForm(entity: Tenant): FormGroup {
  48 + return this.fb.group(
  49 + {
  50 + title: [entity ? entity.title : '', [Validators.required]],
  51 + additionalInfo: this.fb.group(
  52 + {
  53 + description: [entity && entity.additionalInfo ? entity.additionalInfo.description : '']
  54 + }
  55 + )
  56 + }
  57 + );
  58 + }
  59 +
  60 + updateEntityForm(entity: Tenant) {
  61 + this.entityForm.patchValue({title: entity.title});
  62 + this.entityForm.patchValue({additionalInfo: {description: entity.additionalInfo ? entity.additionalInfo.description : ''}});
  63 + }
  64 +
  65 + onTenantIdCopied(event) {
  66 + this.store.dispatch(new ActionNotificationShow(
  67 + {
  68 + message: this.translate.instant('tenant.idCopiedMessage'),
  69 + type: 'success',
  70 + duration: 750,
  71 + verticalPosition: 'bottom',
  72 + horizontalPosition: 'right'
  73 + }));
  74 + }
  75 +
  76 +}
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { NgModule } from '@angular/core';
  18 +import { CommonModule } from '@angular/common';
  19 +import { SharedModule } from '@shared/shared.module';
  20 +import {TenantComponent} from '@modules/home/pages/tenant/tenant.component';
  21 +import {TenantRoutingModule} from '@modules/home/pages/tenant/tenant-routing.module';
  22 +
  23 +@NgModule({
  24 + entryComponents: [
  25 + TenantComponent
  26 + ],
  27 + declarations: [
  28 + TenantComponent
  29 + ],
  30 + imports: [
  31 + CommonModule,
  32 + SharedModule,
  33 + TenantRoutingModule
  34 + ]
  35 +})
  36 +export class TenantModule { }
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Injectable } from '@angular/core';
  18 +
  19 +import { Resolve, Router } from '@angular/router';
  20 +
  21 +import { Tenant } from '@shared/models/tenant.model';
  22 +import {
  23 + DateEntityTableColumn,
  24 + EntityTableColumn,
  25 + EntityTableConfig
  26 +} from '@shared/components/entity/entities-table-config.models';
  27 +import { TenantService } from '@core/http/tenant.service';
  28 +import { TranslateService } from '@ngx-translate/core';
  29 +import { DatePipe } from '@angular/common';
  30 +import {
  31 + EntityType,
  32 + entityTypeResources,
  33 + entityTypeTranslations
  34 +} from '@shared/models/entity-type.models';
  35 +import { TenantComponent } from '@modules/home/pages/tenant/tenant.component';
  36 +import { EntityAction } from '@shared/components/entity/entity-component.models';
  37 +import { User } from '@shared/models/user.model';
  38 +
  39 +@Injectable()
  40 +export class TenantsTableConfigResolver implements Resolve<EntityTableConfig<Tenant>> {
  41 +
  42 + private readonly config: EntityTableConfig<Tenant> = new EntityTableConfig<Tenant>();
  43 +
  44 + constructor(private tenantService: TenantService,
  45 + private translate: TranslateService,
  46 + private datePipe: DatePipe,
  47 + private router: Router) {
  48 +
  49 + this.config.entityType = EntityType.CUSTOMER;
  50 + this.config.entityComponent = TenantComponent;
  51 + this.config.entityTranslations = entityTypeTranslations.get(EntityType.TENANT);
  52 + this.config.entityResources = entityTypeResources.get(EntityType.TENANT);
  53 +
  54 + this.config.columns.push(
  55 + new DateEntityTableColumn<Tenant>('createdTime', 'tenant.created-time', this.datePipe, '150px'),
  56 + new EntityTableColumn<Tenant>('title', 'tenant.title'),
  57 + new EntityTableColumn<Tenant>('email', 'contact.email'),
  58 + new EntityTableColumn<Tenant>('country', 'contact.country'),
  59 + new EntityTableColumn<Tenant>('city', 'contact.city')
  60 + );
  61 +
  62 + this.config.cellActionDescriptors.push(
  63 + {
  64 + name: this.translate.instant('tenant.manage-tenant-admins'),
  65 + icon: 'account_circle',
  66 + isEnabled: () => true,
  67 + onAction: ($event, entity) => this.manageTenantAdmins($event, entity)
  68 + }
  69 + );
  70 +
  71 + this.config.deleteEntityTitle = tenant => this.translate.instant('tenant.delete-tenant-title', { tenantTitle: tenant.title });
  72 + this.config.deleteEntityContent = () => this.translate.instant('tenant.delete-tenant-text');
  73 + this.config.deleteEntitiesTitle = count => this.translate.instant('tenant.delete-tenants-title', {count});
  74 + this.config.deleteEntitiesContent = () => this.translate.instant('tenant.delete-tenants-text');
  75 +
  76 + this.config.entitiesFetchFunction = pageLink => this.tenantService.getTenants(pageLink);
  77 + this.config.loadEntity = id => this.tenantService.getTenant(id.id);
  78 + this.config.saveEntity = tenant => this.tenantService.saveTenant(tenant);
  79 + this.config.deleteEntity = id => this.tenantService.deleteTenant(id.id);
  80 + this.config.onEntityAction = action => this.onTenantAction(action);
  81 + }
  82 +
  83 + resolve(): EntityTableConfig<Tenant> {
  84 + this.config.tableTitle = this.translate.instant('tenant.tenants');
  85 +
  86 + return this.config;
  87 + }
  88 +
  89 + manageTenantAdmins($event: Event, tenant: Tenant) {
  90 + if ($event) {
  91 + $event.stopPropagation();
  92 + }
  93 + this.router.navigateByUrl(`tenants/${tenant.id.id}/users`);
  94 + }
  95 +
  96 + onTenantAction(action: EntityAction<Tenant>): boolean {
  97 + switch (action.action) {
  98 + case 'manageTenantAdmins':
  99 + this.manageTenantAdmins(action.event, action.entity);
  100 + return true;
  101 + }
  102 + return false;
  103 + }
  104 +
  105 +}
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2019 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<form style="min-width: 400px;">
  19 + <mat-toolbar fxLayout="row" color="primary">
  20 + <h2 translate>user.activation-link</h2>
  21 + <span fxFlex></span>
  22 + <button mat-button mat-icon-button
  23 + (click)="close()"
  24 + type="button">
  25 + <mat-icon class="material-icons">close</mat-icon>
  26 + </button>
  27 + </mat-toolbar>
  28 + <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
  29 + </mat-progress-bar>
  30 + <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
  31 + <div mat-dialog-content tb-toast toastTarget="activationLinkDialogContent">
  32 + <div class="mat-content mat-padding" fxLayout="column">
  33 + <span [innerHTML]="'user.activation-link-text' | translate: {activationLink: activationLink}"></span>
  34 + <div fxLayout="row" fxLayoutAlign="start center">
  35 + <pre class="tb-highlight" fxFlex><code>{{ activationLink }}</code></pre>
  36 + <button mat-button mat-icon-button
  37 + color="primary"
  38 + ngxClipboard
  39 + cbContent="{{ activationLink }}"
  40 + (cbOnSuccess)="onActivationLinkCopied()"
  41 + matTooltip="{{ 'user.copy-activation-link' | translate }}"
  42 + matTooltipPosition="above">
  43 + <mat-icon svgIcon="mdi:clipboard-arrow-left"></mat-icon>
  44 + </button>
  45 + </div>
  46 + </div>
  47 + </div>
  48 + <div mat-dialog-actions fxLayout="row">
  49 + <span fxFlex></span>
  50 + <button mat-button color="primary"
  51 + style="margin-right: 20px;"
  52 + type="button"
  53 + cdkFocusInitial
  54 + [disabled]="(isLoading$ | async)"
  55 + (click)="close()">
  56 + {{ 'action.ok' | translate }}
  57 + </button>
  58 + </div>
  59 +</form>
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Component, Inject, OnInit } from '@angular/core';
  18 +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
  19 +import { PageComponent } from '@shared/components/page.component';
  20 +import { Store } from '@ngrx/store';
  21 +import { AppState } from '@core/core.state';
  22 +import { TranslateService } from '@ngx-translate/core';
  23 +import { ActionNotificationShow } from '@core/notification/notification.actions';
  24 +
  25 +export interface ActivationLinkDialogData {
  26 + activationLink: string;
  27 +}
  28 +
  29 +@Component({
  30 + selector: 'tb-activation-link-dialog',
  31 + templateUrl: './activation-link-dialog.component.html'
  32 +})
  33 +export class ActivationLinkDialogComponent extends PageComponent implements OnInit {
  34 +
  35 + activationLink: string;
  36 +
  37 + constructor(protected store: Store<AppState>,
  38 + @Inject(MAT_DIALOG_DATA) public data: ActivationLinkDialogData,
  39 + public dialogRef: MatDialogRef<ActivationLinkDialogComponent, void>,
  40 + private translate: TranslateService) {
  41 + super(store);
  42 + this.activationLink = this.data.activationLink;
  43 + }
  44 +
  45 + ngOnInit(): void {
  46 + }
  47 +
  48 + close(): void {
  49 + this.dialogRef.close();
  50 + }
  51 +
  52 + onActivationLinkCopied() {
  53 + this.store.dispatch(new ActionNotificationShow(
  54 + {
  55 + message: this.translate.instant('user.activation-link-copied-message'),
  56 + type: 'success',
  57 + target: 'activationLinkDialogContent',
  58 + duration: 1200,
  59 + verticalPosition: 'bottom',
  60 + horizontalPosition: 'left'
  61 + }));
  62 + }
  63 +
  64 +}
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2019 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<form (ngSubmit)="add()" style="width: 600px;">
  19 + <mat-toolbar fxLayout="row" color="primary">
  20 + <h2 translate>user.add</h2>
  21 + <span fxFlex></span>
  22 + <div [tb-help]="'user'"></div>
  23 + <button mat-button mat-icon-button
  24 + (click)="cancel()"
  25 + type="button">
  26 + <mat-icon class="material-icons">close</mat-icon>
  27 + </button>
  28 + </mat-toolbar>
  29 + <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
  30 + </mat-progress-bar>
  31 + <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
  32 + <div mat-dialog-content>
  33 + <tb-user></tb-user>
  34 + <mat-form-field class="mat-block">
  35 + <mat-label translate>user.activation-method</mat-label>
  36 + <mat-select matInput [ngModelOptions]="{standalone: true}" [(ngModel)]="activationMethod">
  37 + <mat-option *ngFor="let activationMethod of (activationMethods | enumToArray)" [value]="activationMethods[activationMethod]">
  38 + {{ activationMethodTranslations.get(activationMethods[activationMethod]) | translate }}
  39 + </mat-option>
  40 + </mat-select>
  41 + </mat-form-field>
  42 + </div>
  43 + <div mat-dialog-actions fxLayout="row">
  44 + <span fxFlex></span>
  45 + <button mat-button mat-raised-button color="primary"
  46 + type="submit"
  47 + [disabled]="(isLoading$ | async) || detailsForm.invalid || !detailsForm.dirty">
  48 + {{ 'action.add' | translate }}
  49 + </button>
  50 + <button mat-button color="primary"
  51 + style="margin-right: 20px;"
  52 + type="button"
  53 + cdkFocusInitial
  54 + [disabled]="(isLoading$ | async)"
  55 + (click)="cancel()">
  56 + {{ 'action.cancel' | translate }}
  57 + </button>
  58 + </div>
  59 +</form>
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Component, Inject, OnInit, ViewChild } from '@angular/core';
  18 +import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material';
  19 +import { PageComponent } from '@shared/components/page.component';
  20 +import { Store } from '@ngrx/store';
  21 +import { AppState } from '@core/core.state';
  22 +import { NgForm } from '@angular/forms';
  23 +import { UserComponent } from '@modules/home/pages/user/user.component';
  24 +import { Authority } from '@shared/models/authority.enum';
  25 +import { ActivationMethod, activationMethodTranslations, User } from '@shared/models/user.model';
  26 +import { CustomerId } from '@shared/models/id/customer-id';
  27 +import { UserService } from '@core/http/user.service';
  28 +import { Observable } from 'rxjs';
  29 +import {
  30 + ActivationLinkDialogComponent,
  31 + ActivationLinkDialogData
  32 +} from '@modules/home/pages/user/activation-link-dialog.component';
  33 +import {TenantId} from '@app/shared/models/id/tenant-id';
  34 +
  35 +export interface AddUserDialogData {
  36 + tenantId: string;
  37 + customerId: string;
  38 + authority: Authority;
  39 +}
  40 +
  41 +@Component({
  42 + selector: 'tb-add-user-dialog',
  43 + templateUrl: './add-user-dialog.component.html'
  44 +})
  45 +export class AddUserDialogComponent extends PageComponent implements OnInit {
  46 +
  47 + detailsForm: NgForm;
  48 + user: User;
  49 +
  50 + activationMethods = ActivationMethod;
  51 +
  52 + activationMethodTranslations = activationMethodTranslations;
  53 +
  54 + activationMethod = ActivationMethod.DISPLAY_ACTIVATION_LINK;
  55 +
  56 + @ViewChild(UserComponent, {static: true}) userComponent: UserComponent;
  57 +
  58 + constructor(protected store: Store<AppState>,
  59 + @Inject(MAT_DIALOG_DATA) public data: AddUserDialogData,
  60 + public dialogRef: MatDialogRef<AddUserDialogComponent, User>,
  61 + private userService: UserService,
  62 + private dialog: MatDialog) {
  63 + super(store);
  64 + }
  65 +
  66 + ngOnInit(): void {
  67 + this.user = {} as User;
  68 + this.userComponent.isEdit = true;
  69 + this.userComponent.entity = this.user;
  70 + this.detailsForm = this.userComponent.entityNgForm;
  71 + }
  72 +
  73 + cancel(): void {
  74 + this.dialogRef.close(null);
  75 + }
  76 +
  77 + add(): void {
  78 + if (this.detailsForm.valid) {
  79 + this.user = {...this.user, ...this.userComponent.entityForm.value};
  80 + this.user.authority = this.data.authority;
  81 + this.user.tenantId = new TenantId(this.data.tenantId);
  82 + this.user.customerId = new CustomerId(this.data.customerId);
  83 + const sendActivationEmail = this.activationMethod === ActivationMethod.SEND_ACTIVATION_MAIL;
  84 + this.userService.saveUser(this.user, sendActivationEmail).subscribe(
  85 + (user) => {
  86 + if (this.activationMethod === ActivationMethod.DISPLAY_ACTIVATION_LINK) {
  87 + this.userService.getActivationLink(user.id.id).subscribe(
  88 + (activationLink) => {
  89 + this.displayActivationLink(activationLink).subscribe(
  90 + () => {
  91 + this.dialogRef.close(user);
  92 + }
  93 + );
  94 + }
  95 + );
  96 + } else {
  97 + this.dialogRef.close(user);
  98 + }
  99 + }
  100 + );
  101 + }
  102 + }
  103 +
  104 + displayActivationLink(activationLink: string): Observable<void> {
  105 + return this.dialog.open<ActivationLinkDialogComponent, ActivationLinkDialogData,
  106 + void>(ActivationLinkDialogComponent, {
  107 + disableClose: true,
  108 + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
  109 + data: {
  110 + activationLink
  111 + }
  112 + }).afterClosed();
  113 + }
  114 +}
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { NgModule } from '@angular/core';
  18 +import { RouterModule, Routes } from '@angular/router';
  19 +import { UsersTableConfigResolver } from '@modules/home/pages/user/users-table-config.resolver';
  20 +
  21 +@NgModule({
  22 + imports: [],
  23 + exports: [RouterModule],
  24 + providers: [
  25 + UsersTableConfigResolver
  26 + ]
  27 +})
  28 +export class UserRoutingModule { }
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2019 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<div class="tb-details-buttons">
  19 + <button mat-raised-button color="primary"
  20 + [disabled]="(isLoading$ | async)"
  21 + (click)="onEntityAction($event, 'displayActivationLink')"
  22 + [fxShow]="!isEdit">
  23 + {{'user.display-activation-link' | translate }}
  24 + </button>
  25 + <button mat-raised-button color="primary"
  26 + [disabled]="(isLoading$ | async)"
  27 + (click)="onEntityAction($event, 'resendActivation')"
  28 + [fxShow]="!isEdit">
  29 + {{'user.resend-activation' | translate }}
  30 + </button>
  31 + <button mat-raised-button color="primary"
  32 + [disabled]="(isLoading$ | async)"
  33 + (click)="onEntityAction($event, 'loginAsUser')"
  34 + *ngIf="loginAsUserEnabled$ | async"
  35 + [fxShow]="!isEdit">
  36 + {{ (entity?.authority === authority.TENANT_ADMIN ? 'user.login-as-tenant-admin' : 'user.login-as-customer-user') | translate }}
  37 + </button>
  38 + <button mat-raised-button color="primary"
  39 + [disabled]="(isLoading$ | async)"
  40 + (click)="onEntityAction($event, 'delete')"
  41 + [fxShow]="!hideDelete() && !isEdit">
  42 + {{'user.delete' | translate }}
  43 + </button>
  44 +</div>
  45 +<div class="mat-padding" fxLayout="column">
  46 + <form #entityNgForm="ngForm" [formGroup]="entityForm">
  47 + <fieldset [disabled]="(isLoading$ | async) || !isEdit">
  48 + <mat-form-field class="mat-block">
  49 + <mat-label translate>user.email</mat-label>
  50 + <input matInput formControlName="email" required>
  51 + <mat-error *ngIf="entityForm.get('email').hasError('email')">
  52 + {{ 'user.invalid-email-format' | translate }}
  53 + </mat-error>
  54 + <mat-error *ngIf="entityForm.get('email').hasError('required')">
  55 + {{ 'user.email-required' | translate }}
  56 + </mat-error>
  57 + </mat-form-field>
  58 + <mat-form-field class="mat-block">
  59 + <mat-label translate>user.first-name</mat-label>
  60 + <input matInput formControlName="firstName">
  61 + </mat-form-field>
  62 + <mat-form-field class="mat-block">
  63 + <mat-label translate>user.last-name</mat-label>
  64 + <input matInput formControlName="lastName">
  65 + </mat-form-field>
  66 + <div formGroupName="additionalInfo" fxLayout="column">
  67 + <mat-form-field class="mat-block">
  68 + <mat-label translate>user.description</mat-label>
  69 + <textarea matInput formControlName="description" rows="2"></textarea>
  70 + </mat-form-field>
  71 + <section class="tb-default-dashboard" fxFlex fxLayout="column" *ngIf="entity?.id">
  72 + <section fxFlex fxLayout="column" fxLayout.gt-sm="row">
  73 + <tb-dashboard-autocomplete
  74 + fxFlex
  75 + placeholder="{{ 'user.default-dashboard' | translate }}"
  76 + formControlName="defaultDashboardId"
  77 + [dashboardsScope]="entity?.authority === authority.TENANT_ADMIN ? 'tenant' : 'customer'"
  78 + [tenantId]="entity?.tenantId?.id"
  79 + [customerId]="entity?.customerId?.id"
  80 + [selectFirstDashboard]="false"
  81 + ></tb-dashboard-autocomplete>
  82 + <mat-checkbox fxFlex formControlName="defaultDashboardFullscreen">
  83 + {{ 'user.always-fullscreen' | translate }}
  84 + </mat-checkbox>
  85 + </section>
  86 + </section>
  87 + </div>
  88 + </fieldset>
  89 + </form>
  90 +</div>
... ...
  1 +/**
  2 + * Copyright © 2016-2019 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +@import "../../../../../scss/constants";
  17 +
  18 +:host {
  19 + .tb-default-dashboard {
  20 + tb-dashboard-autocomplete {
  21 + @media #{$mat-gt-sm} {
  22 + padding-right: 12px;
  23 + }
  24 +
  25 + @media #{$mat-lt-md} {
  26 + padding-bottom: 12px;
  27 + }
  28 + }
  29 + mat-checkbox {
  30 + @media #{$mat-gt-sm} {
  31 + margin-top: 16px;
  32 + }
  33 + }
  34 + }
  35 +}
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Component, OnInit } from '@angular/core';
  18 +import { select, Store } from '@ngrx/store';
  19 +import { AppState } from '@core/core.state';
  20 +import { EntityComponent } from '@shared/components/entity/entity.component';
  21 +import { FormBuilder, FormGroup, Validators } from '@angular/forms';
  22 +import { User } from '@shared/models/user.model';
  23 +import { selectAuth, selectUserDetails } from '@core/auth/auth.selectors';
  24 +import { map } from 'rxjs/operators';
  25 +import { Authority } from '@shared/models/authority.enum';
  26 +
  27 +@Component({
  28 + selector: 'tb-user',
  29 + templateUrl: './user.component.html',
  30 + styleUrls: ['./user.component.scss']
  31 +})
  32 +export class UserComponent extends EntityComponent<User> {
  33 +
  34 + authority = Authority;
  35 +
  36 + loginAsUserEnabled$ = this.store.pipe(
  37 + select(selectAuth),
  38 + map((auth) => auth.userTokenAccessEnabled)
  39 + );
  40 +
  41 + constructor(protected store: Store<AppState>,
  42 + public fb: FormBuilder) {
  43 + super(store);
  44 + }
  45 +
  46 + hideDelete() {
  47 + if (this.entitiesTableConfig) {
  48 + return !this.entitiesTableConfig.deleteEnabled(this.entity);
  49 + } else {
  50 + return false;
  51 + }
  52 + }
  53 +
  54 + buildForm(entity: User): FormGroup {
  55 + return this.fb.group(
  56 + {
  57 + email: [entity ? entity.email : '', [Validators.required, Validators.email]],
  58 + firstName: [entity ? entity.firstName : ''],
  59 + lastName: [entity ? entity.lastName : ''],
  60 + additionalInfo: this.fb.group(
  61 + {
  62 + description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''],
  63 + defaultDashboardId: [entity && entity.additionalInfo ? entity.additionalInfo.defaultDashboardId : null],
  64 + defaultDashboardFullscreen: [entity && entity.additionalInfo ? entity.additionalInfo.defaultDashboardFullscreen : false],
  65 + }
  66 + )
  67 + }
  68 + );
  69 + }
  70 +
  71 + updateForm(entity: User) {
  72 + this.entityForm.patchValue({email: entity.email});
  73 + this.entityForm.patchValue({firstName: entity.firstName});
  74 + this.entityForm.patchValue({lastName: entity.lastName});
  75 + this.entityForm.patchValue({additionalInfo: {description: entity.additionalInfo ? entity.additionalInfo.description : ''}});
  76 + this.entityForm.patchValue({additionalInfo:
  77 + {defaultDashboardId: entity.additionalInfo ? entity.additionalInfo.defaultDashboardId : null}});
  78 + this.entityForm.patchValue({additionalInfo:
  79 + {defaultDashboardFullscreen: entity.additionalInfo ? entity.additionalInfo.defaultDashboardFullscreen : false}});
  80 + }
  81 +
  82 +}
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { NgModule } from '@angular/core';
  18 +import { CommonModule } from '@angular/common';
  19 +import { SharedModule } from '@shared/shared.module';
  20 +import { UserComponent } from '@modules/home/pages/user/user.component';
  21 +import { UserRoutingModule } from '@modules/home/pages/user/user-routing.module';
  22 +import { AddUserDialogComponent } from '@modules/home/pages/user/add-user-dialog.component';
  23 +import { ActivationLinkDialogComponent } from '@modules/home/pages/user/activation-link-dialog.component';
  24 +
  25 +@NgModule({
  26 + entryComponents: [
  27 + UserComponent,
  28 + AddUserDialogComponent,
  29 + ActivationLinkDialogComponent
  30 + ],
  31 + declarations: [
  32 + UserComponent,
  33 + AddUserDialogComponent,
  34 + ActivationLinkDialogComponent
  35 + ],
  36 + imports: [
  37 + CommonModule,
  38 + SharedModule,
  39 + UserRoutingModule
  40 + ]
  41 +})
  42 +export class UserModule { }
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Injectable } from '@angular/core';
  18 +
  19 +import { ActivatedRouteSnapshot, Resolve } from '@angular/router';
  20 +import {
  21 + DateEntityTableColumn,
  22 + EntityTableColumn,
  23 + EntityTableConfig
  24 +} from '@shared/components/entity/entities-table-config.models';
  25 +import { TranslateService } from '@ngx-translate/core';
  26 +import { DatePipe } from '@angular/common';
  27 +import {
  28 + EntityType,
  29 + entityTypeResources,
  30 + entityTypeTranslations
  31 +} from '@shared/models/entity-type.models';
  32 +import { User } from '@shared/models/user.model';
  33 +import { UserService } from '@core/http/user.service';
  34 +import { UserComponent } from '@modules/home/pages/user/user.component';
  35 +import { CustomerService } from '@core/http/customer.service';
  36 +import { map, mergeMap, take, tap } from 'rxjs/operators';
  37 +import { forkJoin, noop, Observable, of } from 'rxjs';
  38 +import { Authority } from '@shared/models/authority.enum';
  39 +import { CustomerId } from '@shared/models/id/customer-id';
  40 +import { MatDialog } from '@angular/material';
  41 +import { EntityAction } from '@shared/components/entity/entity-component.models';
  42 +import {
  43 + AddUserDialogComponent,
  44 + AddUserDialogData
  45 +} from '@modules/home/pages/user/add-user-dialog.component';
  46 +import { AuthState } from '@core/auth/auth.models';
  47 +import { select, Store } from '@ngrx/store';
  48 +import { AppState } from '@core/core.state';
  49 +import { selectAuth } from '@core/auth/auth.selectors';
  50 +import { AuthService } from '@core/auth/auth.service';
  51 +import {
  52 + ActivationLinkDialogComponent,
  53 + ActivationLinkDialogData
  54 +} from '@modules/home/pages/user/activation-link-dialog.component';
  55 +import { ActionNotificationShow } from '@core/notification/notification.actions';
  56 +import { NULL_UUID } from '@shared/models/id/has-uuid';
  57 +import { Customer } from '@shared/models/customer.model';
  58 +import {TenantService} from '@app/core/http/tenant.service';
  59 +import {TenantId} from '@app/shared/models/id/tenant-id';
  60 +
  61 +export interface UsersTableRouteData {
  62 + authority: Authority;
  63 +}
  64 +
  65 +@Injectable()
  66 +export class UsersTableConfigResolver implements Resolve<EntityTableConfig<User>> {
  67 +
  68 + private readonly config: EntityTableConfig<User> = new EntityTableConfig<User>();
  69 +
  70 + private tenantId: string;
  71 + private customerId: string;
  72 + private authority: Authority;
  73 + private authUser: User;
  74 +
  75 + constructor(private store: Store<AppState>,
  76 + private userService: UserService,
  77 + private authService: AuthService,
  78 + private tenantService: TenantService,
  79 + private customerService: CustomerService,
  80 + private translate: TranslateService,
  81 + private datePipe: DatePipe,
  82 + private dialog: MatDialog) {
  83 +
  84 + this.config.entityType = EntityType.USER;
  85 + this.config.entityComponent = UserComponent;
  86 + this.config.entityTranslations = entityTypeTranslations.get(EntityType.USER);
  87 + this.config.entityResources = entityTypeResources.get(EntityType.USER);
  88 +
  89 + this.config.columns.push(
  90 + new DateEntityTableColumn<User>('createdTime', 'user.created-time', this.datePipe, '150px'),
  91 + new EntityTableColumn<User>('firstName', 'user.first-name'),
  92 + new EntityTableColumn<User>('lastName', 'user.last-name'),
  93 + new EntityTableColumn<User>('email', 'user.email')
  94 + );
  95 +
  96 + this.config.deleteEnabled = user => user && user.id && user.id.id !== this.authUser.id.id;
  97 + this.config.deleteEntityTitle = user => this.translate.instant('user.delete-user-title', { userEmail: user.email });
  98 + this.config.deleteEntityContent = () => this.translate.instant('user.delete-user-text');
  99 + this.config.deleteEntitiesTitle = count => this.translate.instant('user.delete-users-title', {count});
  100 + this.config.deleteEntitiesContent = () => this.translate.instant('user.delete-users-text');
  101 +
  102 + this.config.loadEntity = id => this.userService.getUser(id.id);
  103 + this.config.saveEntity = user => this.saveUser(user);
  104 + this.config.deleteEntity = id => this.userService.deleteUser(id.id);
  105 + this.config.onEntityAction = action => this.onUserAction(action);
  106 + this.config.addEntity = () => this.addUser();
  107 + }
  108 +
  109 + resolve(route: ActivatedRouteSnapshot): Observable<EntityTableConfig<User>> {
  110 + const routeParams = route.params;
  111 + return this.store.pipe(select(selectAuth), take(1)).pipe(
  112 + tap((auth) => {
  113 + this.authUser = auth.userDetails;
  114 + this.authority = routeParams.tenantId ? Authority.TENANT_ADMIN : Authority.CUSTOMER_USER;
  115 + if (this.authority === Authority.TENANT_ADMIN) {
  116 + this.tenantId = routeParams.tenantId;
  117 + this.customerId = NULL_UUID;
  118 + this.config.entitiesFetchFunction = pageLink => this.userService.getTenantAdmins(this.tenantId, pageLink);
  119 + } else {
  120 + this.tenantId = this.authUser.tenantId.id;
  121 + this.customerId = routeParams.customerId;
  122 + this.config.entitiesFetchFunction = pageLink => this.userService.getCustomerUsers(this.customerId, pageLink);
  123 + }
  124 + this.updateActionCellDescriptors(auth);
  125 + }),
  126 + mergeMap(() => this.authority === Authority.TENANT_ADMIN ?
  127 + this.tenantService.getTenant(this.tenantId) :
  128 + this.customerService.getCustomer(this.customerId)),
  129 + map((parentEntity) => {
  130 + if (this.authority === Authority.TENANT_ADMIN) {
  131 + this.config.tableTitle = parentEntity.title + ': ' + this.translate.instant('user.tenant-admins');
  132 + } else {
  133 + this.config.tableTitle = parentEntity.title + ': ' + this.translate.instant('user.customer-users');
  134 + }
  135 + return this.config;
  136 + })
  137 + );
  138 + }
  139 +
  140 + updateActionCellDescriptors(auth: AuthState) {
  141 + this.config.cellActionDescriptors.splice(0);
  142 + if (auth.userTokenAccessEnabled) {
  143 + this.config.cellActionDescriptors.push(
  144 + {
  145 + name: this.authority === Authority.TENANT_ADMIN ?
  146 + this.translate.instant('user.login-as-tenant-admin') :
  147 + this.translate.instant('user.login-as-customer-user'),
  148 + icon: 'mdi:login',
  149 + isMdiIcon: true,
  150 + isEnabled: () => true,
  151 + onAction: ($event, entity) => this.loginAsUser($event, entity)
  152 + }
  153 + );
  154 + }
  155 + }
  156 +
  157 + saveUser(user: User): Observable<User> {
  158 + user.tenantId = new TenantId(this.tenantId);
  159 + user.customerId = new CustomerId(this.customerId);
  160 + user.authority = this.authority;
  161 + return this.userService.saveUser(user);
  162 + }
  163 +
  164 + addUser(): Observable<User> {
  165 + return this.dialog.open<AddUserDialogComponent, AddUserDialogData,
  166 + User>(AddUserDialogComponent, {
  167 + disableClose: true,
  168 + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
  169 + data: {
  170 + tenantId: this.tenantId,
  171 + customerId: this.customerId,
  172 + authority: this.authority
  173 + }
  174 + }).afterClosed();
  175 + }
  176 +
  177 + loginAsUser($event: Event, user: User) {
  178 + if ($event) {
  179 + $event.stopPropagation();
  180 + }
  181 + this.authService.loginAsUser(user.id.id).subscribe();
  182 + }
  183 +
  184 + displayActivationLink($event: Event, user: User) {
  185 + if ($event) {
  186 + $event.stopPropagation();
  187 + }
  188 + this.userService.getActivationLink(user.id.id).subscribe(
  189 + (activationLink) => {
  190 + this.dialog.open<ActivationLinkDialogComponent, ActivationLinkDialogData,
  191 + void>(ActivationLinkDialogComponent, {
  192 + disableClose: true,
  193 + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
  194 + data: {
  195 + activationLink
  196 + }
  197 + });
  198 + }
  199 + );
  200 + }
  201 +
  202 + resendActivation($event: Event, user: User) {
  203 + if ($event) {
  204 + $event.stopPropagation();
  205 + }
  206 + this.userService.sendActivationEmail(user.email).subscribe(() => {
  207 + this.store.dispatch(new ActionNotificationShow(
  208 + {
  209 + message: this.translate.instant('user.activation-email-sent-message'),
  210 + type: 'success'
  211 + }));
  212 + });
  213 + }
  214 +
  215 + onUserAction(action: EntityAction<User>): boolean {
  216 + switch (action.action) {
  217 + case 'loginAsUser':
  218 + this.loginAsUser(action.event, action.entity);
  219 + return true;
  220 + case 'displayActivationLink':
  221 + this.displayActivationLink(action.event, action.entity);
  222 + return true;
  223 + case 'resendActivation':
  224 + this.resendActivation(action.event, action.entity);
  225 + return true;
  226 + }
  227 + return false;
  228 + }
  229 +
  230 +}
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2019 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<section [formGroup]="parentForm">
  19 + <mat-form-field class="mat-block">
  20 + <mat-label translate>contact.country</mat-label>
  21 + <mat-select matInput formControlName="country">
  22 + <mat-option *ngFor="let country of countries" [value]="country">
  23 + {{ country }}
  24 + </mat-option>
  25 + </mat-select>
  26 + </mat-form-field>
  27 + <div fxLayout.gt-sm="row" fxLayoutGap.gt-sm="10px">
  28 + <mat-form-field class="mat-block">
  29 + <mat-label translate>contact.city</mat-label>
  30 + <input matInput formControlName="city">
  31 + </mat-form-field>
  32 + <mat-form-field class="mat-block">
  33 + <mat-label translate>contact.state</mat-label>
  34 + <input matInput formControlName="state">
  35 + </mat-form-field>
  36 + <mat-form-field class="mat-block">
  37 + <mat-label translate>contact.postal-code</mat-label>
  38 + <input matInput formControlName="zip">
  39 + <mat-error *ngIf="parentForm.get('zip').hasError('pattern')">
  40 + {{ 'contact.postal-code-invalid' | translate }}
  41 + </mat-error>
  42 + </mat-form-field>
  43 + </div>
  44 + <mat-form-field class="mat-block">
  45 + <mat-label translate>contact.address</mat-label>
  46 + <input matInput formControlName="address">
  47 + </mat-form-field>
  48 + <mat-form-field class="mat-block">
  49 + <mat-label translate>contact.address2</mat-label>
  50 + <input matInput formControlName="address2">
  51 + </mat-form-field>
  52 + <mat-form-field class="mat-block">
  53 + <mat-label translate>contact.phone</mat-label>
  54 + <input matInput formControlName="phone">
  55 + </mat-form-field>
  56 + <mat-form-field class="mat-block">
  57 + <mat-label translate>contact.email</mat-label>
  58 + <input matInput formControlName="email">
  59 + <mat-error *ngIf="parentForm.get('email').hasError('email')">
  60 + {{ 'user.invalid-email-format' | translate }}
  61 + </mat-error>
  62 + </mat-form-field>
  63 +</section>
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Component, Input } from '@angular/core';
  18 +import { FormGroup } from '@angular/forms';
  19 +import { COUNTRIES } from '@shared/components/contact.models';
  20 +
  21 +@Component({
  22 + selector: 'tb-contact',
  23 + templateUrl: './contact.component.html'
  24 +})
  25 +export class ContactComponent {
  26 +
  27 + @Input()
  28 + parentForm: FormGroup;
  29 +
  30 + @Input() isEdit: boolean;
  31 +
  32 + countries = COUNTRIES;
  33 +
  34 +}
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +export const COUNTRIES = [
  18 + 'Afghanistan',
  19 + 'Åland Islands',
  20 + 'Albania',
  21 + 'Algeria',
  22 + 'American Samoa',
  23 + 'Andorra',
  24 + 'Angola',
  25 + 'Anguilla',
  26 + 'Antarctica',
  27 + 'Antigua and Barbuda',
  28 + 'Argentina',
  29 + 'Armenia',
  30 + 'Aruba',
  31 + 'Australia',
  32 + 'Austria',
  33 + 'Azerbaijan',
  34 + 'Bahamas',
  35 + 'Bahrain',
  36 + 'Bangladesh',
  37 + 'Barbados',
  38 + 'Belarus',
  39 + 'Belgium',
  40 + 'Belize',
  41 + 'Benin',
  42 + 'Bermuda',
  43 + 'Bhutan',
  44 + 'Bolivia',
  45 + 'Bonaire, Sint Eustatius and Saba',
  46 + 'Bosnia and Herzegovina',
  47 + 'Botswana',
  48 + 'Bouvet Island',
  49 + 'Brazil',
  50 + 'British Indian Ocean Territory',
  51 + 'Brunei Darussalam',
  52 + 'Bulgaria',
  53 + 'Burkina Faso',
  54 + 'Burundi',
  55 + 'Cambodia',
  56 + 'Cameroon',
  57 + 'Canada',
  58 + 'Cape Verde',
  59 + 'Cayman Islands',
  60 + 'Central African Republic',
  61 + 'Chad',
  62 + 'Chile',
  63 + 'China',
  64 + 'Christmas Island',
  65 + 'Cocos (Keeling) Islands',
  66 + 'Colombia',
  67 + 'Comoros',
  68 + 'Congo',
  69 + 'Congo, The Democratic Republic of the',
  70 + 'Cook Islands',
  71 + 'Costa Rica',
  72 + 'Côte d\'Ivoire',
  73 + 'Croatia',
  74 + 'Cuba',
  75 + 'Curaçao',
  76 + 'Cyprus',
  77 + 'Czech Republic',
  78 + 'Denmark',
  79 + 'Djibouti',
  80 + 'Dominica',
  81 + 'Dominican Republic',
  82 + 'Ecuador',
  83 + 'Egypt',
  84 + 'El Salvador',
  85 + 'Equatorial Guinea',
  86 + 'Eritrea',
  87 + 'Estonia',
  88 + 'Ethiopia',
  89 + 'Falkland Islands (Malvinas)',
  90 + 'Faroe Islands',
  91 + 'Fiji',
  92 + 'Finland',
  93 + 'France',
  94 + 'French Guiana',
  95 + 'French Polynesia',
  96 + 'French Southern Territories',
  97 + 'Gabon',
  98 + 'Gambia',
  99 + 'Georgia',
  100 + 'Germany',
  101 + 'Ghana',
  102 + 'Gibraltar',
  103 + 'Greece',
  104 + 'Greenland',
  105 + 'Grenada',
  106 + 'Guadeloupe',
  107 + 'Guam',
  108 + 'Guatemala',
  109 + 'Guernsey',
  110 + 'Guinea',
  111 + 'Guinea-Bissau',
  112 + 'Guyana',
  113 + 'Haiti',
  114 + 'Heard Island and McDonald Islands',
  115 + 'Holy See (Vatican City State)',
  116 + 'Honduras',
  117 + 'Hong Kong',
  118 + 'Hungary',
  119 + 'Iceland',
  120 + 'India',
  121 + 'Indonesia',
  122 + 'Iran, Islamic Republic of',
  123 + 'Iraq',
  124 + 'Ireland',
  125 + 'Isle of Man',
  126 + 'Israel',
  127 + 'Italy',
  128 + 'Jamaica',
  129 + 'Japan',
  130 + 'Jersey',
  131 + 'Jordan',
  132 + 'Kazakhstan',
  133 + 'Kenya',
  134 + 'Kiribati',
  135 + 'Korea, Democratic People\'s Republic of',
  136 + 'Korea, Republic of',
  137 + 'Kuwait',
  138 + 'Kyrgyzstan',
  139 + 'Lao People\'s Democratic Republic',
  140 + 'Latvia',
  141 + 'Lebanon',
  142 + 'Lesotho',
  143 + 'Liberia',
  144 + 'Libya',
  145 + 'Liechtenstein',
  146 + 'Lithuania',
  147 + 'Luxembourg',
  148 + 'Macao',
  149 + 'Macedonia, Republic Of',
  150 + 'Madagascar',
  151 + 'Malawi',
  152 + 'Malaysia',
  153 + 'Maldives',
  154 + 'Mali',
  155 + 'Malta',
  156 + 'Marshall Islands',
  157 + 'Martinique',
  158 + 'Mauritania',
  159 + 'Mauritius',
  160 + 'Mayotte',
  161 + 'Mexico',
  162 + 'Micronesia, Federated States of',
  163 + 'Moldova, Republic of',
  164 + 'Monaco',
  165 + 'Mongolia',
  166 + 'Montenegro',
  167 + 'Montserrat',
  168 + 'Morocco',
  169 + 'Mozambique',
  170 + 'Myanmar',
  171 + 'Namibia',
  172 + 'Nauru',
  173 + 'Nepal',
  174 + 'Netherlands',
  175 + 'New Caledonia',
  176 + 'New Zealand',
  177 + 'Nicaragua',
  178 + 'Niger',
  179 + 'Nigeria',
  180 + 'Niue',
  181 + 'Norfolk Island',
  182 + 'Northern Mariana Islands',
  183 + 'Norway',
  184 + 'Oman',
  185 + 'Pakistan',
  186 + 'Palau',
  187 + 'Palestinian Territory, Occupied',
  188 + 'Panama',
  189 + 'Papua New Guinea',
  190 + 'Paraguay',
  191 + 'Peru',
  192 + 'Philippines',
  193 + 'Pitcairn',
  194 + 'Poland',
  195 + 'Portugal',
  196 + 'Puerto Rico',
  197 + 'Qatar',
  198 + 'Reunion',
  199 + 'Romania',
  200 + 'Russian Federation',
  201 + 'Rwanda',
  202 + 'Saint Barthélemy',
  203 + 'Saint Helena, Ascension and Tristan da Cunha',
  204 + 'Saint Kitts and Nevis',
  205 + 'Saint Lucia',
  206 + 'Saint Martin (French Part)',
  207 + 'Saint Pierre and Miquelon',
  208 + 'Saint Vincent and the Grenadines',
  209 + 'Samoa',
  210 + 'San Marino',
  211 + 'Sao Tome and Principe',
  212 + 'Saudi Arabia',
  213 + 'Senegal',
  214 + 'Serbia',
  215 + 'Seychelles',
  216 + 'Sierra Leone',
  217 + 'Singapore',
  218 + 'Sint Maarten (Dutch Part)',
  219 + 'Slovakia',
  220 + 'Slovenia',
  221 + 'Solomon Islands',
  222 + 'Somalia',
  223 + 'South Africa',
  224 + 'South Georgia and the South Sandwich Islands',
  225 + 'South Sudan',
  226 + 'Spain',
  227 + 'Sri Lanka',
  228 + 'Sudan',
  229 + 'Suriname',
  230 + 'Svalbard and Jan Mayen',
  231 + 'Swaziland',
  232 + 'Sweden',
  233 + 'Switzerland',
  234 + 'Syrian Arab Republic',
  235 + 'Taiwan',
  236 + 'Tajikistan',
  237 + 'Tanzania, United Republic of',
  238 + 'Thailand',
  239 + 'Timor-Leste',
  240 + 'Togo',
  241 + 'Tokelau',
  242 + 'Tonga',
  243 + 'Trinidad and Tobago',
  244 + 'Tunisia',
  245 + 'Turkey',
  246 + 'Turkmenistan',
  247 + 'Turks and Caicos Islands',
  248 + 'Tuvalu',
  249 + 'Uganda',
  250 + 'Ukraine',
  251 + 'United Arab Emirates',
  252 + 'United Kingdom',
  253 + 'United States',
  254 + 'United States Minor Outlying Islands',
  255 + 'Uruguay',
  256 + 'Uzbekistan',
  257 + 'Vanuatu',
  258 + 'Venezuela',
  259 + 'Viet Nam',
  260 + 'Virgin Islands, British',
  261 + 'Virgin Islands, U.S.',
  262 + 'Wallis and Futuna',
  263 + 'Western Sahara',
  264 + 'Yemen',
  265 + 'Zambia',
  266 + 'Zimbabwe'
  267 +];
  268 +
  269 +/* tslint:disable */
  270 +export const POSTAL_CODE_PATTERNS = {
  271 + 'United States': '(\\d{5}([\\-]\\d{4})?)',
  272 + 'Australia': '[0-9]{4}',
  273 + 'Austria': '[0-9]{4}',
  274 + 'Belgium': '[0-9]{4}',
  275 + 'Brazil': '[0-9]{5}[\\-]?[0-9]{3}',
  276 + 'Canada': '^(?!.*[DFIOQU])[A-VXY][0-9][A-Z][ -]?[0-9][A-Z][0-9]$',
  277 + 'Denmark': '[0-9]{3,4}',
  278 + 'Faroe Islands': '[0-9]{3,4}',
  279 + 'Netherlands': '[1-9][0-9]{3}\\s?[a-zA-Z]{2}',
  280 + 'Germany': '[0-9]{5}',
  281 + 'Hungary': '[0-9]{4}',
  282 + 'Italy': '[0-9]{5}',
  283 + 'Japan': '\\d{3}-\\d{4}',
  284 + 'Luxembourg': '(L\\s*(-|—|–))\\s*?[\\d]{4}',
  285 + 'Poland': '[0-9]{2}\\-[0-9]{3}',
  286 + 'Spain': '((0[1-9]|5[0-2])|[1-4][0-9])[0-9]{3}',
  287 + 'Sweden': '\\d{3}\\s?\\d{2}',
  288 + 'United Kingdom': '[A-Za-z]{1,2}[0-9Rr][0-9A-Za-z]? [0-9][ABD-HJLNP-UW-Zabd-hjlnp-uw-z]{2}'
  289 +};
  290 +/* tslint:enable */
  291 +
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2019 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<mat-form-field [formGroup]="selectDashboardFormGroup" class="mat-block">
  19 + <input matInput type="text" placeholder="{{ placeholder || ('dashboard.dashboard' | translate) }}"
  20 + #dashboardInput
  21 + formControlName="dashboard"
  22 + [required]="required"
  23 + [matAutocomplete]="dashboardAutocomplete">
  24 + <button *ngIf="selectDashboardFormGroup.get('dashboard').value && !disabled"
  25 + type="button"
  26 + matSuffix mat-button mat-icon-button aria-label="Clear"
  27 + (click)="clear()">
  28 + <mat-icon class="material-icons">close</mat-icon>
  29 + </button>
  30 + <mat-autocomplete #dashboardAutocomplete="matAutocomplete" [displayWith]="displayDashboardFn">
  31 + <mat-option *ngFor="let dashboard of filteredDashboards | async" [value]="dashboard">
  32 + <span [innerHTML]="dashboard.title | highlight:searchText"></span>
  33 + </mat-option>
  34 + <mat-option *ngIf="!(filteredDashboards | async)?.length" [value]="null">
  35 + <span>
  36 + {{ translate.get('dashboard.no-dashboards-matching', {entity: searchText}) | async }}
  37 + </span>
  38 + </mat-option>
  39 + </mat-autocomplete>
  40 + <mat-error>
  41 + <ng-content select="[tb-error]"></ng-content>
  42 + </mat-error>
  43 + <mat-hint>
  44 + <ng-content select="[tb-hint]"></ng-content>
  45 + </mat-hint>
  46 +</mat-form-field>
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import {AfterViewInit, Component, ElementRef, forwardRef, Input, OnInit, ViewChild} from '@angular/core';
  18 +import {ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR} from '@angular/forms';
  19 +import {Observable, of} from 'rxjs';
  20 +import {PageLink} from '@shared/models/page/page-link';
  21 +import {Direction} from '@shared/models/page/sort-order';
  22 +import {map, mergeMap, startWith, tap} from 'rxjs/operators';
  23 +import {PageData, emptyPageData} from '@shared/models/page/page-data';
  24 +import {DashboardInfo} from '@app/shared/models/dashboard.models';
  25 +import {DashboardId} from '@app/shared/models/id/dashboard-id';
  26 +import {DashboardService} from '@core/http/dashboard.service';
  27 +import {Store} from '@ngrx/store';
  28 +import {AppState} from '@app/core/core.state';
  29 +import {getCurrentAuthUser} from '@app/core/auth/auth.selectors';
  30 +import {Authority} from '@shared/models/authority.enum';
  31 +import {TranslateService} from '@ngx-translate/core';
  32 +
  33 +@Component({
  34 + selector: 'tb-dashboard-autocomplete',
  35 + templateUrl: './dashboard-autocomplete.component.html',
  36 + styleUrls: [],
  37 + providers: [{
  38 + provide: NG_VALUE_ACCESSOR,
  39 + useExisting: forwardRef(() => DashboardAutocompleteComponent),
  40 + multi: true
  41 + }]
  42 +})
  43 +export class DashboardAutocompleteComponent implements ControlValueAccessor, OnInit, AfterViewInit {
  44 +
  45 + selectDashboardFormGroup: FormGroup;
  46 +
  47 + modelValue: DashboardInfo | string | null;
  48 +
  49 + @Input()
  50 + useIdValue = true;
  51 +
  52 + @Input()
  53 + selectFirstDashboard = false;
  54 +
  55 + @Input()
  56 + placeholder: string;
  57 +
  58 + @Input()
  59 + dashboardsScope: 'customer' | 'tenant';
  60 +
  61 + @Input()
  62 + tenantId: string;
  63 +
  64 + @Input()
  65 + customerId: string;
  66 +
  67 + @Input()
  68 + required: boolean;
  69 +
  70 + @Input()
  71 + disabled: boolean;
  72 +
  73 + @ViewChild('dashboardInput', {static: true}) dashboardInput: ElementRef;
  74 +
  75 + filteredDashboards: Observable<Array<DashboardInfo>>;
  76 +
  77 + private valueLoaded = false;
  78 +
  79 + private searchText = '';
  80 +
  81 + private propagateChange = (v: any) => { };
  82 +
  83 + constructor(private store: Store<AppState>,
  84 + public translate: TranslateService,
  85 + private dashboardService: DashboardService,
  86 + private fb: FormBuilder) {
  87 + this.selectDashboardFormGroup = this.fb.group({
  88 + dashboard: [null]
  89 + });
  90 + }
  91 +
  92 + registerOnChange(fn: any): void {
  93 + this.propagateChange = fn;
  94 + }
  95 +
  96 + registerOnTouched(fn: any): void {
  97 + }
  98 +
  99 + ngOnInit() {
  100 +
  101 + }
  102 +
  103 + ngAfterViewInit(): void {
  104 + this.selectFirstDashboardIfNeeded();
  105 + }
  106 +
  107 + selectFirstDashboardIfNeeded(): void {
  108 + if (this.selectFirstDashboard && !this.modelValue) {
  109 + this.getDashboards(new PageLink(1)).subscribe(
  110 + (data) => {
  111 + if (data.data.length) {
  112 + const dashboard = data.data[0];
  113 + this.modelValue = this.useIdValue ? dashboard.id.id : dashboard;
  114 + this.selectDashboardFormGroup.get('dashboard').patchValue(dashboard, {emitEvent: false});
  115 + this.propagateChange(this.modelValue);
  116 + }
  117 + }
  118 + );
  119 + }
  120 + }
  121 +
  122 + setDisabledState(isDisabled: boolean): void {
  123 + this.disabled = isDisabled;
  124 + }
  125 +
  126 + initFilteredResults(): void {
  127 + this.filteredDashboards = this.selectDashboardFormGroup.get('dashboard').valueChanges
  128 + .pipe(
  129 + startWith<string | DashboardInfo>(''),
  130 + tap(value => {
  131 + if (this.valueLoaded) {
  132 + let modelValue;
  133 + if (typeof value === 'string' || !value) {
  134 + modelValue = null;
  135 + } else {
  136 + modelValue = this.useIdValue ? value.id.id : value;
  137 + }
  138 + this.updateView(modelValue);
  139 + }
  140 + }),
  141 + map(value => value ? (typeof value === 'string' ? value : value.name) : ''),
  142 + mergeMap(name => this.fetchDashboards(name) )
  143 + );
  144 + }
  145 +
  146 + writeValue(value: DashboardInfo | string | null): void {
  147 + this.valueLoaded = false;
  148 + this.searchText = '';
  149 + this.initFilteredResults();
  150 + if (value != null) {
  151 + if (typeof value === 'string') {
  152 + this.dashboardService.getDashboardInfo(value).subscribe(
  153 + (dashboard) => {
  154 + this.modelValue = this.useIdValue ? dashboard.id.id : dashboard;
  155 + this.selectDashboardFormGroup.get('dashboard').patchValue(dashboard, {emitEvent: true});
  156 + this.valueLoaded = true;
  157 + }
  158 + );
  159 + } else {
  160 + this.modelValue = this.useIdValue ? value.id.id : value;
  161 + this.selectDashboardFormGroup.get('dashboard').patchValue(value, {emitEvent: false});
  162 + this.valueLoaded = true;
  163 + }
  164 + } else {
  165 + this.modelValue = null;
  166 + this.selectDashboardFormGroup.get('dashboard').patchValue(null, {emitEvent: false});
  167 + this.valueLoaded = true;
  168 + }
  169 + }
  170 +
  171 + updateView(value: DashboardInfo | string | null) {
  172 + if (this.modelValue !== value) {
  173 + this.modelValue = value;
  174 + this.propagateChange(this.modelValue);
  175 + }
  176 + }
  177 +
  178 + displayDashboardFn(dashboard?: DashboardInfo): string | undefined {
  179 + return dashboard ? dashboard.title : undefined;
  180 + }
  181 +
  182 + fetchDashboards(searchText?: string): Observable<Array<DashboardInfo>> {
  183 + this.searchText = searchText;
  184 + const pageLink = new PageLink(10, 0, searchText, {
  185 + property: 'title',
  186 + direction: Direction.ASC
  187 + });
  188 + return this.getDashboards(pageLink).pipe(
  189 + map(pageData => {
  190 + return pageData.data;
  191 + })
  192 + );
  193 + }
  194 +
  195 + getDashboards(pageLink: PageLink): Observable<PageData<DashboardInfo>> {
  196 + let dashboardsObservable: Observable<PageData<DashboardInfo>>;
  197 + const authUser = getCurrentAuthUser(this.store);
  198 + if (this.dashboardsScope === 'customer' || authUser.authority === Authority.CUSTOMER_USER) {
  199 + if (this.customerId) {
  200 + dashboardsObservable = this.dashboardService.getCustomerDashboards(this.customerId, pageLink, false, true);
  201 + } else {
  202 + dashboardsObservable = of(emptyPageData());
  203 + }
  204 + } else {
  205 + if (authUser.authority === Authority.SYS_ADMIN) {
  206 + if (this.tenantId) {
  207 + dashboardsObservable = this.dashboardService.getTenantDashboardsByTenantId(this.tenantId, pageLink, false, true);
  208 + } else {
  209 + dashboardsObservable = of(emptyPageData());
  210 + }
  211 + } else {
  212 + dashboardsObservable = this.dashboardService.getTenantDashboards(pageLink, false, true);
  213 + }
  214 + }
  215 + return dashboardsObservable;
  216 + }
  217 +
  218 + clear() {
  219 + this.selectDashboardFormGroup.get('dashboard').patchValue(null, {emitEvent: true});
  220 + setTimeout(() => {
  221 + this.dashboardInput.nativeElement.blur();
  222 + this.dashboardInput.nativeElement.focus();
  223 + }, 0);
  224 + }
  225 +
  226 +}
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2019 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<header>
  19 + <mat-toolbar color="primary" [ngStyle]="{height: headerHeightPx+'px'}">
  20 + <div fxFlex fxLayout="row" fxLayoutAlign="start center">
  21 + <div class="mat-toolbar-tools" fxFlex fxLayout="column" fxLayoutAlign="start start">
  22 + <span class="tb-details-title">{{ headerTitle }}</span>
  23 + <span class="tb-details-subtitle">{{ headerSubtitle }}</span>
  24 + <span style="width: 100%;">
  25 + <ng-content select=".header-pane"></ng-content>
  26 + </span>
  27 + </div>
  28 + <ng-content select=".details-buttons"></ng-content>
  29 + <button mat-button mat-icon-button (click)="onCloseDetails()">
  30 + <mat-icon class="material-icons">close</mat-icon>
  31 + </button>
  32 + </div>
  33 + <section *ngIf="!isReadOnly" fxLayout="row" class="layout-wrap tb-header-buttons">
  34 + <button [disabled]="(isLoading$ | async) || theForm.invalid || !theForm.dirty"
  35 + mat-fab
  36 + matTooltip="{{ 'action.apply-changes' | translate }}"
  37 + matTooltipPosition="above"
  38 + color="accent" class="tb-btn-header mat-fab-bottom-right"
  39 + [ngClass]="{'tb-hide': !isEdit}"
  40 + (click)="onApplyDetails()">
  41 + <mat-icon class="material-icons">done</mat-icon>
  42 + </button>
  43 + <button [disabled]="(isLoading$ | async) || (isAlwaysEdit && !theForm.dirty)"
  44 + mat-fab
  45 + matTooltip="{{ (isAlwaysEdit ? 'action.decline-changes' : 'details.toggle-edit-mode') | translate }}"
  46 + matTooltipPosition="above"
  47 + color="accent" class="tb-btn-header mat-fab-bottom-right"
  48 + (click)="onToggleDetailsEditMode()">
  49 + <mat-icon class="material-icons">{{isEdit ? 'close' : 'edit'}}</mat-icon>
  50 + </button>
  51 + </section>
  52 + </mat-toolbar>
  53 +</header>
  54 +<div fxFlex class="mat-content">
  55 + <ng-content></ng-content>
  56 +</div>
... ...
  1 +/**
  2 + * Copyright © 2016-2019 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +@import '../../../scss/constants';
  17 +
  18 +:host {
  19 + width: 100%;
  20 + height: 100%;
  21 + display: flex;
  22 + flex-direction: column;
  23 + .mat-toolbar-tools {
  24 + height: 100%;
  25 + min-height: 100px;
  26 + max-height: 120px;
  27 + }
  28 + .tb-details-title {
  29 + width: inherit;
  30 + margin: 20px 8px 0 0;
  31 + overflow: hidden;
  32 + font-size: 1rem;
  33 + font-weight: 400;
  34 + text-overflow: ellipsis;
  35 + text-transform: uppercase;
  36 + white-space: nowrap;
  37 +
  38 + @media #{$mat-gt-sm} {
  39 + font-size: 1.6rem;
  40 + }
  41 + }
  42 +
  43 + .tb-details-subtitle {
  44 + width: inherit;
  45 + margin: 10px 0;
  46 + overflow: hidden;
  47 + font-size: 1rem;
  48 + text-overflow: ellipsis;
  49 + white-space: nowrap;
  50 + opacity: .8;
  51 + }
  52 +
  53 +}
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Component, EventEmitter, Input, Output } from '@angular/core';
  18 +import { PageComponent } from '@shared/components/page.component';
  19 +import { Store } from '@ngrx/store';
  20 +import { AppState } from '@core/core.state';
  21 +import { NgForm } from '@angular/forms';
  22 +
  23 +@Component({
  24 + selector: 'tb-details-panel',
  25 + templateUrl: './details-panel.component.html',
  26 + styleUrls: ['./details-panel.component.scss']
  27 +})
  28 +export class DetailsPanelComponent extends PageComponent {
  29 +
  30 + @Input() headerHeightPx = 100;
  31 + @Input() headerTitle = '';
  32 + @Input() headerSubtitle = '';
  33 + @Input() isReadOnly = false;
  34 + @Input() isAlwaysEdit = false;
  35 + @Input() theForm: NgForm;
  36 + @Output()
  37 + closeDetails = new EventEmitter<void>();
  38 + @Output()
  39 + toggleDetailsEditMode = new EventEmitter<boolean>();
  40 + @Output()
  41 + applyDetails = new EventEmitter<void>();
  42 +
  43 + isEditValue = false;
  44 +
  45 + @Output()
  46 + isEditChange = new EventEmitter<boolean>();
  47 +
  48 + @Input()
  49 + get isEdit() {
  50 + return this.isEditValue;
  51 + }
  52 +
  53 + set isEdit(val: boolean) {
  54 + this.isEditValue = val;
  55 + this.isEditChange.emit(this.isEditValue);
  56 + }
  57 +
  58 +
  59 + constructor(protected store: Store<AppState>) {
  60 + super(store);
  61 + }
  62 +
  63 + onCloseDetails() {
  64 + this.closeDetails.emit();
  65 + }
  66 +
  67 + onToggleDetailsEditMode() {
  68 + if (!this.isAlwaysEdit) {
  69 + this.isEdit = !this.isEdit;
  70 + }
  71 + this.toggleDetailsEditMode.emit(this.isEditValue);
  72 + }
  73 +
  74 + onApplyDetails() {
  75 + if (this.theForm.valid) {
  76 + this.applyDetails.emit();
  77 + }
  78 + }
  79 +
  80 +}
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2019 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<form (ngSubmit)="add()" style="min-width: 400px;">
  19 + <mat-toolbar fxLayout="row" color="primary">
  20 + <h2 translate>{{ translations.add }}</h2>
  21 + <span fxFlex></span>
  22 + <div [tb-help]="resources.helpLinkId"></div>
  23 + <button mat-button mat-icon-button
  24 + (click)="cancel()"
  25 + type="button">
  26 + <mat-icon class="material-icons">close</mat-icon>
  27 + </button>
  28 + </mat-toolbar>
  29 + <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
  30 + </mat-progress-bar>
  31 + <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
  32 + <div mat-dialog-content>
  33 + <tb-anchor #entityDetailsForm></tb-anchor>
  34 + </div>
  35 + <div mat-dialog-actions fxLayout="row">
  36 + <span fxFlex></span>
  37 + <button mat-button mat-raised-button color="primary"
  38 + type="submit"
  39 + [disabled]="(isLoading$ | async) || detailsForm.invalid || !detailsForm.dirty">
  40 + {{ 'action.add' | translate }}
  41 + </button>
  42 + <button mat-button color="primary"
  43 + style="margin-right: 20px;"
  44 + type="button"
  45 + cdkFocusInitial
  46 + [disabled]="(isLoading$ | async)"
  47 + (click)="cancel()">
  48 + {{ 'action.cancel' | translate }}
  49 + </button>
  50 + </div>
  51 +</form>
... ...
  1 +/**
  2 + * Copyright © 2016-2019 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +:host {
  17 +}
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import {
  18 + Component,
  19 + ComponentFactoryResolver,
  20 + Inject,
  21 + OnInit,
  22 + SkipSelf,
  23 + ViewChild
  24 +} from '@angular/core';
  25 +import { ErrorStateMatcher, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
  26 +import { PageComponent } from '@shared/components/page.component';
  27 +import { Store } from '@ngrx/store';
  28 +import { AppState } from '@core/core.state';
  29 +import { FormControl, FormGroupDirective, NgForm } from '@angular/forms';
  30 +import { EntityTypeResource, EntityTypeTranslation } from '@shared/models/entity-type.models';
  31 +import { EntityTableConfig } from '@shared/components/entity/entities-table-config.models';
  32 +import { BaseData, HasId } from '@shared/models/base-data';
  33 +import { EntityId } from '@shared/models/id/entity-id';
  34 +import { AddEntityDialogData } from '@shared/components/entity/entity-component.models';
  35 +import { TbAnchorComponent } from '@shared/components/tb-anchor.component';
  36 +import { EntityComponent } from '@shared/components/entity/entity.component';
  37 +
  38 +@Component({
  39 + selector: 'tb-add-entity-dialog',
  40 + templateUrl: './add-entity-dialog.component.html',
  41 + providers: [{provide: ErrorStateMatcher, useExisting: AddEntityDialogComponent}],
  42 + styleUrls: ['./add-entity-dialog.component.scss']
  43 +})
  44 +export class AddEntityDialogComponent extends PageComponent implements OnInit, ErrorStateMatcher {
  45 +
  46 + entityComponent: EntityComponent<BaseData<HasId>>;
  47 + detailsForm: NgForm;
  48 +
  49 + entitiesTableConfig: EntityTableConfig<BaseData<HasId>>;
  50 + translations: EntityTypeTranslation;
  51 + resources: EntityTypeResource;
  52 + entity: BaseData<EntityId>;
  53 +
  54 + submitted = false;
  55 +
  56 + @ViewChild('entityDetailsForm', {static: true}) entityDetailsFormAnchor: TbAnchorComponent;
  57 +
  58 + constructor(protected store: Store<AppState>,
  59 + @Inject(MAT_DIALOG_DATA) public data: AddEntityDialogData<BaseData<HasId>>,
  60 + public dialogRef: MatDialogRef<AddEntityDialogComponent, BaseData<HasId>>,
  61 + private componentFactoryResolver: ComponentFactoryResolver,
  62 + @SkipSelf() private errorStateMatcher: ErrorStateMatcher) {
  63 + super(store);
  64 + }
  65 +
  66 + ngOnInit(): void {
  67 + this.entitiesTableConfig = this.data.entitiesTableConfig;
  68 + this.translations = this.entitiesTableConfig.entityTranslations;
  69 + this.resources = this.entitiesTableConfig.entityResources;
  70 + this.entity = {};
  71 + const componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.entitiesTableConfig.entityComponent);
  72 + const viewContainerRef = this.entityDetailsFormAnchor.viewContainerRef;
  73 + viewContainerRef.clear();
  74 + const componentRef = viewContainerRef.createComponent(componentFactory);
  75 + this.entityComponent = componentRef.instance;
  76 + this.entityComponent.isEdit = true;
  77 + this.entityComponent.entitiesTableConfig = this.entitiesTableConfig;
  78 + this.entityComponent.entity = this.entity;
  79 + this.detailsForm = this.entityComponent.entityNgForm;
  80 + }
  81 +
  82 + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
  83 + const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
  84 + const customErrorState = !!(control && control.invalid && this.submitted);
  85 + return originalErrorState || customErrorState;
  86 + }
  87 +
  88 + cancel(): void {
  89 + this.dialogRef.close(null);
  90 + }
  91 +
  92 + add(): void {
  93 + this.submitted = true;
  94 + if (this.detailsForm.valid) {
  95 + this.entity = {...this.entity, ...this.entityComponent.entityFormValue()};
  96 + this.entitiesTableConfig.saveEntity(this.entity).subscribe(
  97 + (entity) => {
  98 + this.dialogRef.close(entity);
  99 + }
  100 + );
  101 + }
  102 + }
  103 +}
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Store } from '@ngrx/store';
  18 +import { AppState } from '@core/core.state';
  19 +import { EntityComponent } from '@shared/components/entity/entity.component';
  20 +import { FormBuilder, FormGroup, ValidatorFn, Validators } from '@angular/forms';
  21 +import { ContactBased } from '@shared/models/contact-based.model';
  22 +import { AfterViewInit } from '@angular/core';
  23 +import { POSTAL_CODE_PATTERNS } from '@shared/components/contact.models';
  24 +import { HasId } from '@shared/models/base-data';
  25 +
  26 +export abstract class ContactBasedComponent<T extends ContactBased<HasId>> extends EntityComponent<T> implements AfterViewInit {
  27 +
  28 + constructor(protected store: Store<AppState>,
  29 + protected fb: FormBuilder) {
  30 + super(store);
  31 + }
  32 +
  33 + buildForm(entity: T): FormGroup {
  34 + const entityForm = this.buildEntityForm(entity);
  35 + entityForm.addControl('country', this.fb.control(entity ? entity.country : '', []));
  36 + entityForm.addControl('city', this.fb.control(entity ? entity.city : '', []));
  37 + entityForm.addControl('state', this.fb.control(entity ? entity.state : '', []));
  38 + entityForm.addControl('zip', this.fb.control(entity ? entity.zip : '',
  39 + this.zipValidators(entity ? entity.country : '')
  40 + ));
  41 + entityForm.addControl('address', this.fb.control(entity ? entity.address : '', []));
  42 + entityForm.addControl('address2', this.fb.control(entity ? entity.address2 : '', []));
  43 + entityForm.addControl('phone', this.fb.control(entity ? entity.phone : '', []));
  44 + entityForm.addControl('email', this.fb.control(entity ? entity.email : '', [Validators.email]));
  45 + return entityForm;
  46 + }
  47 +
  48 + updateForm(entity: T) {
  49 + this.updateEntityForm(entity);
  50 + this.entityForm.patchValue({country: entity.country});
  51 + this.entityForm.patchValue({city: entity.city});
  52 + this.entityForm.patchValue({state: entity.state});
  53 + this.entityForm.get('zip').setValidators(this.zipValidators(entity.country));
  54 + this.entityForm.patchValue({zip: entity.zip});
  55 + this.entityForm.patchValue({address: entity.address});
  56 + this.entityForm.patchValue({address2: entity.address2});
  57 + this.entityForm.patchValue({phone: entity.phone});
  58 + this.entityForm.patchValue({email: entity.email});
  59 + }
  60 +
  61 + ngAfterViewInit() {
  62 + this.entityForm.get('country').valueChanges.subscribe(
  63 + (country) => {
  64 + this.entityForm.get('zip').setValidators(this.zipValidators(country));
  65 + this.entityForm.get('zip').updateValueAndValidity({onlySelf: true});
  66 + this.entityForm.get('zip').markAsTouched({onlySelf: true});
  67 + }
  68 + );
  69 + }
  70 +
  71 + zipValidators(country: string): ValidatorFn[] {
  72 + const zipValidators = [];
  73 + if (country && POSTAL_CODE_PATTERNS[country]) {
  74 + const postalCodePattern = POSTAL_CODE_PATTERNS[country];
  75 + zipValidators.push(Validators.pattern(postalCodePattern));
  76 + }
  77 + return zipValidators;
  78 + }
  79 +
  80 + abstract buildEntityForm(entity: T): FormGroup;
  81 +
  82 + abstract updateEntityForm(entity: T);
  83 +
  84 +}
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { BaseData, HasId } from '@shared/models/base-data';
  18 +import { EntityId } from '@shared/models/id/entity-id';
  19 +import { EntitiesFetchFunction } from '@shared/models/datasource/entity-datasource';
  20 +import { Observable, of } from 'rxjs';
  21 +import { emptyPageData } from '@shared/models/page/page-data';
  22 +import { DatePipe } from '@angular/common';
  23 +import { Direction, SortOrder } from '@shared/models/page/sort-order';
  24 +import {
  25 + EntityType,
  26 + EntityTypeResource,
  27 + EntityTypeTranslation
  28 +} from '@shared/models/entity-type.models';
  29 +import { EntityComponent } from '@shared/components/entity/entity.component';
  30 +import { Type } from '@angular/core';
  31 +import { EntityAction } from '@shared/components/entity/entity-component.models';
  32 +import { HasUUID } from '@shared/models/id/has-uuid';
  33 +import { PageLink } from '@shared/models/page/page-link';
  34 +import { EntitiesTableComponent } from '@shared/components/entity/entities-table.component';
  35 +import { EntityTableHeaderComponent } from '@shared/components/entity/entity-table-header.component';
  36 +import { ActivatedRoute } from '@angular/router';
  37 +
  38 +export type EntityBooleanFunction<T extends BaseData<HasId>> = (entity: T) => boolean;
  39 +export type EntityStringFunction<T extends BaseData<HasId>> = (entity: T) => string;
  40 +export type EntityCountStringFunction = (count: number) => string;
  41 +export type EntityTwoWayOperation<T extends BaseData<HasId>> = (entity: T) => Observable<T>;
  42 +export type EntityByIdOperation<T extends BaseData<HasId>> = (id: HasUUID) => Observable<T>;
  43 +export type EntityIdOneWayOperation = (id: HasUUID) => Observable<any>;
  44 +export type EntityActionFunction<T extends BaseData<HasId>> = (action: EntityAction<T>) => boolean;
  45 +export type CreateEntityOperation<T extends BaseData<HasId>> = () => Observable<T>;
  46 +
  47 +export type CellContentFunction<T extends BaseData<HasId>> = (entity: T, key: string) => string;
  48 +export type CellStyleFunction<T extends BaseData<HasId>> = (entity: T, key: string) => object;
  49 +
  50 +export interface CellActionDescriptor<T extends BaseData<HasId>> {
  51 + name: string;
  52 + nameFunction?: (entity: T) => string;
  53 + icon?: string;
  54 + isMdiIcon?: boolean;
  55 + color?: string;
  56 + isEnabled: (entity: T) => boolean;
  57 + onAction: ($event: MouseEvent, entity: T) => void;
  58 +}
  59 +
  60 +export interface GroupActionDescriptor<T extends BaseData<HasId>> {
  61 + name: string;
  62 + icon: string;
  63 + isEnabled: boolean;
  64 + onAction: ($event: MouseEvent, entities: T[]) => void;
  65 +}
  66 +
  67 +export interface HeaderActionDescriptor {
  68 + name: string;
  69 + icon: string;
  70 + isEnabled: () => boolean;
  71 + onAction: ($event: MouseEvent) => void;
  72 +}
  73 +
  74 +export class EntityTableColumn<T extends BaseData<HasId>> {
  75 + constructor(public key: string,
  76 + public title: string,
  77 + public maxWidth: string = '100%',
  78 + public cellContentFunction: CellContentFunction<T> = (entity, property) => entity[property],
  79 + public cellStyleFunction: CellStyleFunction<T> = () => ({})) {
  80 + }
  81 +}
  82 +
  83 +export class DateEntityTableColumn<T extends BaseData<HasId>> extends EntityTableColumn<T> {
  84 + constructor(key: string,
  85 + title: string,
  86 + datePipe: DatePipe,
  87 + maxWidth: string = '100%',
  88 + dateFormat: string = 'yyyy-MM-dd HH:mm:ss',
  89 + cellStyleFunction: CellStyleFunction<T> = () => ({})) {
  90 + super(key,
  91 + title,
  92 + maxWidth,
  93 + (entity, property) => datePipe.transform(entity[property], dateFormat),
  94 + cellStyleFunction);
  95 + }
  96 +}
  97 +
  98 +export class EntityTableConfig<T extends BaseData<HasId>, P extends PageLink = PageLink> {
  99 +
  100 + constructor() {}
  101 +
  102 + componentsData: any = null;
  103 +
  104 + loadDataOnInit = true;
  105 + onLoadAction: (route: ActivatedRoute) => void = null;
  106 + table: EntitiesTableComponent = null;
  107 + useTimePageLink = false;
  108 + entityType: EntityType = null;
  109 + tableTitle = '';
  110 + selectionEnabled = true;
  111 + searchEnabled = true;
  112 + addEnabled = true;
  113 + entitiesDeleteEnabled = true;
  114 + detailsPanelEnabled = true;
  115 + actionsColumnTitle = null;
  116 + entityTranslations: EntityTypeTranslation;
  117 + entityResources: EntityTypeResource;
  118 + entityComponent: Type<EntityComponent<T>>;
  119 + defaultSortOrder: SortOrder = {property: 'createdTime', direction: Direction.ASC};
  120 + columns: Array<EntityTableColumn<T>> = [];
  121 + cellActionDescriptors: Array<CellActionDescriptor<T>> = [];
  122 + groupActionDescriptors: Array<GroupActionDescriptor<T>> = [];
  123 + headerActionDescriptors: Array<HeaderActionDescriptor> = [];
  124 + headerComponent: Type<EntityTableHeaderComponent<T>>;
  125 + addEntity: CreateEntityOperation<T> = null;
  126 + detailsReadonly: EntityBooleanFunction<T> = () => false;
  127 + deleteEnabled: EntityBooleanFunction<T> = () => true;
  128 + deleteEntityTitle: EntityStringFunction<T> = () => '';
  129 + deleteEntityContent: EntityStringFunction<T> = () => '';
  130 + deleteEntitiesTitle: EntityCountStringFunction = () => '';
  131 + deleteEntitiesContent: EntityCountStringFunction = () => '';
  132 + loadEntity: EntityByIdOperation<T> = () => of();
  133 + saveEntity: EntityTwoWayOperation<T> = (entity) => of(entity);
  134 + deleteEntity: EntityIdOneWayOperation = () => of();
  135 + entitiesFetchFunction: EntitiesFetchFunction<T, P> = () => of(emptyPageData<T>());
  136 + onEntityAction: EntityActionFunction<T> = () => false;
  137 +}
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2019 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<mat-drawer-container hasBackdrop="false" class="tb-absolute-fill">
  19 + <mat-drawer *ngIf="entitiesTableConfig.detailsPanelEnabled"
  20 + class="tb-details-drawer mat-elevation-z4"
  21 + #drawer
  22 + mode="over"
  23 + position="end"
  24 + [opened]="isDetailsOpen">
  25 + <tb-entity-details-panel
  26 + [entitiesTableConfig]="entitiesTableConfig"
  27 + [entityId]="dataSource.currentEntity?.id"
  28 + (closeEntityDetails)="isDetailsOpen = false"
  29 + (entityUpdated)="onEntityUpdated($event)"
  30 + (entityAction)="onEntityAction($event)"
  31 + >
  32 + </tb-entity-details-panel>
  33 + </mat-drawer>
  34 + <mat-drawer-content>
  35 + <div class="mat-padding tb-entity-table tb-absolute-fill">
  36 + <div fxFlex fxLayout="column" class="mat-elevation-z1 tb-entity-table-content">
  37 + <mat-toolbar class="mat-table-toolbar" [fxShow]="!textSearchMode && dataSource.selection.isEmpty()">
  38 + <div class="mat-toolbar-tools">
  39 + <span *ngIf="entitiesTableConfig.tableTitle" class="tb-entity-table-title">{{ entitiesTableConfig.tableTitle }}</span>
  40 + <tb-timewindow *ngIf="entitiesTableConfig.useTimePageLink" [(ngModel)]="timewindow"
  41 + (ngModelChange)="onTimewindowChange()"
  42 + asButton historyOnly></tb-timewindow>
  43 + <tb-anchor #entityTableHeader></tb-anchor>
  44 + <span fxFlex *ngIf="!this.entitiesTableConfig.headerComponent"></span>
  45 + <button mat-button mat-icon-button [disabled]="isLoading$ | async" [fxShow]="addEnabled()" (click)="addEntity($event)"
  46 + matTooltip="{{ translations.add | translate }}"
  47 + matTooltipPosition="above">
  48 + <mat-icon>add</mat-icon>
  49 + </button>
  50 + <button mat-button mat-icon-button [disabled]="isLoading$ | async"
  51 + [fxShow]="actionDescriptor.isEnabled()" *ngFor="let actionDescriptor of headerActionDescriptors"
  52 + matTooltip="{{ actionDescriptor.name }}"
  53 + matTooltipPosition="above"
  54 + (click)="actionDescriptor.onAction($event)">
  55 + <mat-icon>{{actionDescriptor.icon}}</mat-icon>
  56 + </button>
  57 + <button mat-button mat-icon-button [disabled]="isLoading$ | async" (click)="updateData()"
  58 + matTooltip="{{ 'action.refresh' | translate }}"
  59 + matTooltipPosition="above">
  60 + <mat-icon>refresh</mat-icon>
  61 + </button>
  62 + <button *ngIf="entitiesTableConfig.searchEnabled"
  63 + mat-button mat-icon-button [disabled]="isLoading$ | async" (click)="enterFilterMode()"
  64 + matTooltip="{{ translations.search | translate }}"
  65 + matTooltipPosition="above">
  66 + <mat-icon>search</mat-icon>
  67 + </button>
  68 + </div>
  69 + </mat-toolbar>
  70 + <mat-toolbar class="mat-table-toolbar" [fxShow]="textSearchMode && dataSource.selection.isEmpty()">
  71 + <div class="mat-toolbar-tools">
  72 + <button mat-button mat-icon-button
  73 + matTooltip="{{ translations.search | translate }}"
  74 + matTooltipPosition="above">
  75 + <mat-icon>search</mat-icon>
  76 + </button>
  77 + <mat-form-field fxFlex>
  78 + <mat-label>&nbsp;</mat-label>
  79 + <input #searchInput matInput
  80 + [(ngModel)]="pageLink.textSearch"
  81 + placeholder="{{ translations.search | translate }}"/>
  82 + </mat-form-field>
  83 + <button mat-button mat-icon-button (click)="exitFilterMode()"
  84 + matTooltip="{{ 'action.close' | translate }}"
  85 + matTooltipPosition="above">
  86 + <mat-icon>close</mat-icon>
  87 + </button>
  88 + </div>
  89 + </mat-toolbar>
  90 + <mat-toolbar *ngIf="entitiesTableConfig.selectionEnabled" class="mat-table-toolbar" color="primary" [fxShow]="!dataSource.selection.isEmpty()">
  91 + <div class="mat-toolbar-tools">
  92 + <span>
  93 + {{ translate.get(translations.selectedEntities, {count: dataSource.selection.selected.length}) | async }}
  94 + </span>
  95 + <span fxFlex></span>
  96 + <button mat-button mat-icon-button [disabled]="isLoading$ | async"
  97 + [fxShow]="actionDescriptor.isEnabled" *ngFor="let actionDescriptor of groupActionDescriptors"
  98 + matTooltip="{{ actionDescriptor.name }}"
  99 + matTooltipPosition="above"
  100 + (click)="actionDescriptor.onAction($event, dataSource.selection.selected)">
  101 + <mat-icon>{{actionDescriptor.icon}}</mat-icon>
  102 + </button>
  103 + </div>
  104 + </mat-toolbar>
  105 + <div fxFlex class="table-container">
  106 + <mat-table [dataSource]="dataSource"
  107 + matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="(pageLink.sortOrder.direction + '').toLowerCase()" matSortDisableClear>
  108 + <ng-container matColumnDef="select" sticky>
  109 + <mat-header-cell *matHeaderCellDef>
  110 + <mat-checkbox (change)="$event ? dataSource.masterToggle() : null"
  111 + [checked]="dataSource.selection.hasValue() && (dataSource.isAllSelected() | async)"
  112 + [indeterminate]="dataSource.selection.hasValue() && !(dataSource.isAllSelected() | async)">
  113 + </mat-checkbox>
  114 + </mat-header-cell>
  115 + <mat-cell *matCellDef="let entity">
  116 + <mat-checkbox (click)="$event.stopPropagation()"
  117 + (change)="$event ? dataSource.selection.toggle(entity) : null"
  118 + [checked]="dataSource.selection.isSelected(entity)">
  119 + </mat-checkbox>
  120 + </mat-cell>
  121 + </ng-container>
  122 + <ng-container [matColumnDef]="column.key" *ngFor="let column of columns">
  123 + <mat-header-cell *matHeaderCellDef [ngStyle]="{maxWidth: column.maxWidth}" mat-sort-header> {{ column.title | translate }} </mat-header-cell>
  124 + <mat-cell *matCellDef="let entity" [ngStyle]="cellStyle(entity, column)" [innerHTML]="cellContent(entity, column)"></mat-cell>
  125 + </ng-container>
  126 + <ng-container matColumnDef="actions" stickyEnd>
  127 + <mat-header-cell *matHeaderCellDef [ngStyle.gt-md]="{ minWidth: (cellActionDescriptors.length * 40) + 'px' }">
  128 + {{ entitiesTableConfig.actionsColumnTitle ? (entitiesTableConfig.actionsColumnTitle | translate) : '' }}
  129 + </mat-header-cell>
  130 + <mat-cell *matCellDef="let entity" [ngStyle.gt-md]="{ minWidth: (cellActionDescriptors.length * 40) + 'px' }">
  131 + <div fxHide fxShow.gt-md fxFlex fxLayout="row" fxLayoutAlign="end">
  132 + <button mat-button mat-icon-button [disabled]="isLoading$ | async"
  133 + [fxShow]="actionDescriptor.isEnabled(entity)" *ngFor="let actionDescriptor of cellActionDescriptors"
  134 + matTooltip="{{ actionDescriptor.nameFunction ? actionDescriptor.nameFunction(entity) : actionDescriptor.name }}"
  135 + matTooltipPosition="above"
  136 + (click)="actionDescriptor.onAction($event, entity)">
  137 + <mat-icon *ngIf="!actionDescriptor.isMdiIcon" [ngStyle]="actionDescriptor.color ? {color: actionDescriptor.color} : {}">
  138 + {{actionDescriptor.icon}}</mat-icon>
  139 + <mat-icon *ngIf="actionDescriptor.isMdiIcon" [ngStyle]="actionDescriptor.color ? {color: actionDescriptor.color} : {}"
  140 + [svgIcon]="actionDescriptor.icon"></mat-icon>
  141 + </button>
  142 + </div>
  143 + <div fxHide fxShow.lt-lg>
  144 + <button mat-button mat-icon-button
  145 + (click)="$event.stopPropagation()"
  146 + [matMenuTriggerFor]="cellActionsMenu">
  147 + <mat-icon class="material-icons">more_vert</mat-icon>
  148 + </button>
  149 + <mat-menu #cellActionsMenu="matMenu" xPosition="before">
  150 + <button mat-menu-item *ngFor="let actionDescriptor of cellActionDescriptors"
  151 + [disabled]="isLoading$ | async"
  152 + [fxShow]="actionDescriptor.isEnabled(entity)"
  153 + (click)="actionDescriptor.onAction($event, entity)">
  154 + <mat-icon *ngIf="!actionDescriptor.isMdiIcon" [ngStyle]="actionDescriptor.color ? {color: actionDescriptor.color} : {}">
  155 + {{actionDescriptor.icon}}</mat-icon>
  156 + <mat-icon *ngIf="actionDescriptor.isMdiIcon" [ngStyle]="actionDescriptor.color ? {color: actionDescriptor.color} : {}"
  157 + [svgIcon]="actionDescriptor.icon"></mat-icon>
  158 + <span>{{ actionDescriptor.nameFunction ? actionDescriptor.nameFunction(entity) : actionDescriptor.name }}</span>
  159 + </button>
  160 + </mat-menu>
  161 + </div>
  162 + </mat-cell>
  163 + </ng-container>
  164 + <mat-header-row [ngClass]="{'mat-row-select': selectionEnabled}" *matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row>
  165 + <mat-row [ngClass]="{'mat-row-select': selectionEnabled,
  166 + 'mat-selected': dataSource.selection.isSelected(entity),
  167 + 'tb-current-entity': dataSource.isCurrentEntity(entity)}"
  168 + *matRowDef="let entity; columns: displayedColumns;" (click)="onRowClick($event, entity)"></mat-row>
  169 + </mat-table>
  170 + <span [fxShow]="dataSource.isEmpty() | async"
  171 + fxLayoutAlign="center center"
  172 + class="no-data-found" translate>{{ translations.noEntities }}</span>
  173 + </div>
  174 + <mat-divider></mat-divider>
  175 + <mat-paginator [length]="dataSource.total() | async"
  176 + [pageIndex]="pageLink.page"
  177 + [pageSize]="pageLink.pageSize"
  178 + [pageSizeOptions]="[10, 20, 30]"></mat-paginator>
  179 + </div>
  180 + </div>
  181 + </mat-drawer-content>
  182 +</mat-drawer-container>
... ...
  1 +/**
  2 + * Copyright © 2016-2019 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +:host {
  17 + width: 100%;
  18 + height: 100%;
  19 + .tb-entity-table {
  20 + .tb-entity-table-content {
  21 + width: 100%;
  22 + height: 100%;
  23 + background: #fff;
  24 +
  25 + .tb-entity-table-title {
  26 + padding-right: 20px;
  27 + white-space: nowrap;
  28 + overflow: hidden;
  29 + text-overflow: ellipsis;
  30 + }
  31 +
  32 + .table-container {
  33 + overflow: auto;
  34 + }
  35 + }
  36 + }
  37 +}
  38 +
  39 +:host ::ng-deep .mat-sort-header-sorted .mat-sort-header-arrow {
  40 + opacity: 1 !important;
  41 +}
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import {
  18 + AfterViewInit,
  19 + Component, ComponentFactoryResolver,
  20 + ElementRef,
  21 + Input,
  22 + OnInit,
  23 + Type,
  24 + ViewChild
  25 +} from '@angular/core';
  26 +import { PageComponent } from '@shared/components/page.component';
  27 +import { Store } from '@ngrx/store';
  28 +import { AppState } from '@core/core.state';
  29 +import { PageLink, TimePageLink } from '@shared/models/page/page-link';
  30 +import { MatDialog, MatPaginator, MatSort } from '@angular/material';
  31 +import { EntitiesDataSource } from '@shared/models/datasource/entity-datasource';
  32 +import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators';
  33 +import { Direction, SortOrder } from '@shared/models/page/sort-order';
  34 +import { forkJoin, fromEvent, merge, Observable } from 'rxjs';
  35 +import { TranslateService } from '@ngx-translate/core';
  36 +import { BaseData, HasId } from '@shared/models/base-data';
  37 +import { EntityId } from '@shared/models/id/entity-id';
  38 +import { ActivatedRoute } from '@angular/router';
  39 +import {
  40 + CellActionDescriptor,
  41 + EntityTableColumn,
  42 + EntityTableConfig,
  43 + GroupActionDescriptor,
  44 + HeaderActionDescriptor
  45 +} from '@shared/components/entity/entities-table-config.models';
  46 +import { EntityTypeTranslation } from '@shared/models/entity-type.models';
  47 +import { DialogService } from '@core/services/dialog.service';
  48 +import { AddEntityDialogComponent } from '@shared/components/entity/add-entity-dialog.component';
  49 +import {
  50 + AddEntityDialogData,
  51 + EntityAction
  52 +} from '@shared/components/entity/entity-component.models';
  53 +import { Timewindow } from '@shared/models/time/time.models';
  54 +import { DomSanitizer } from '@angular/platform-browser';
  55 +import { TbAnchorComponent } from '@shared/components/tb-anchor.component';
  56 +
  57 +@Component({
  58 + selector: 'tb-entities-table',
  59 + templateUrl: './entities-table.component.html',
  60 + styleUrls: ['./entities-table.component.scss']
  61 +})
  62 +export class EntitiesTableComponent extends PageComponent implements AfterViewInit, OnInit {
  63 +
  64 + @Input()
  65 + entitiesTableConfig: EntityTableConfig<BaseData<HasId>>;
  66 +
  67 + translations: EntityTypeTranslation;
  68 +
  69 + headerActionDescriptors: Array<HeaderActionDescriptor>;
  70 + groupActionDescriptors: Array<GroupActionDescriptor<BaseData<HasId>>>;
  71 + cellActionDescriptors: Array<CellActionDescriptor<BaseData<HasId>>>;
  72 +
  73 + columns: Array<EntityTableColumn<BaseData<HasId>>>;
  74 + displayedColumns: string[] = [];
  75 +
  76 + selectionEnabled;
  77 +
  78 + pageLink: PageLink;
  79 + textSearchMode = false;
  80 + timewindow: Timewindow;
  81 + dataSource: EntitiesDataSource<BaseData<HasId>>;
  82 +
  83 + isDetailsOpen = false;
  84 +
  85 + @ViewChild('entityTableHeader', {static: false}) entityTableHeaderAnchor: TbAnchorComponent;
  86 +
  87 + @ViewChild('searchInput', {static: false}) searchInputField: ElementRef;
  88 +
  89 + @ViewChild(MatPaginator, {static: false}) paginator: MatPaginator;
  90 + @ViewChild(MatSort, {static: false}) sort: MatSort;
  91 +
  92 + constructor(protected store: Store<AppState>,
  93 + private route: ActivatedRoute,
  94 + public translate: TranslateService,
  95 + public dialog: MatDialog,
  96 + private dialogService: DialogService,
  97 + private domSanitizer: DomSanitizer,
  98 + private componentFactoryResolver: ComponentFactoryResolver) {
  99 + super(store);
  100 + }
  101 +
  102 + ngOnInit() {
  103 + this.entitiesTableConfig = this.entitiesTableConfig || this.route.snapshot.data.entitiesTableConfig;
  104 + if (this.entitiesTableConfig.headerComponent) {
  105 + const componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.entitiesTableConfig.headerComponent);
  106 + const viewContainerRef = this.entityTableHeaderAnchor.viewContainerRef;
  107 + viewContainerRef.clear();
  108 + const componentRef = viewContainerRef.createComponent(componentFactory);
  109 + const headerComponent = componentRef.instance;
  110 + headerComponent.entitiesTableConfig = this.entitiesTableConfig;
  111 + }
  112 +
  113 + this.entitiesTableConfig.table = this;
  114 + this.translations = this.entitiesTableConfig.entityTranslations;
  115 +
  116 + this.headerActionDescriptors = [...this.entitiesTableConfig.headerActionDescriptors];
  117 + this.groupActionDescriptors = [...this.entitiesTableConfig.groupActionDescriptors];
  118 + this.cellActionDescriptors = [...this.entitiesTableConfig.cellActionDescriptors];
  119 +
  120 + if (this.entitiesTableConfig.entitiesDeleteEnabled) {
  121 + this.cellActionDescriptors.push(
  122 + {
  123 + name: this.translate.instant('action.delete'),
  124 + icon: 'delete',
  125 + isEnabled: entity => this.entitiesTableConfig.deleteEnabled(entity),
  126 + onAction: ($event, entity) => this.deleteEntity($event, entity)
  127 + }
  128 + );
  129 + }
  130 +
  131 + this.groupActionDescriptors.push(
  132 + {
  133 + name: this.translate.instant('action.delete'),
  134 + icon: 'delete',
  135 + isEnabled: this.entitiesTableConfig.entitiesDeleteEnabled,
  136 + onAction: ($event, entities) => this.deleteEntities($event, entities)
  137 + }
  138 + );
  139 +
  140 + this.columns = [...this.entitiesTableConfig.columns];
  141 +
  142 + this.selectionEnabled = this.entitiesTableConfig.selectionEnabled;
  143 +
  144 + if (this.selectionEnabled) {
  145 + this.displayedColumns.push('select');
  146 + }
  147 + this.columns.forEach(
  148 + (column) => {
  149 + this.displayedColumns.push(column.key);
  150 + }
  151 + );
  152 + this.displayedColumns.push('actions');
  153 +
  154 + const sortOrder: SortOrder = { property: this.entitiesTableConfig.defaultSortOrder.property,
  155 + direction: this.entitiesTableConfig.defaultSortOrder.direction };
  156 +
  157 + if (this.entitiesTableConfig.useTimePageLink) {
  158 + this.timewindow = Timewindow.historyInterval(24 * 60 * 60 * 1000);
  159 + const currentTime = new Date().getTime();
  160 + this.pageLink = new TimePageLink(10, 0, null, sortOrder,
  161 + currentTime - this.timewindow.history.timewindowMs, currentTime);
  162 + } else {
  163 + this.pageLink = new PageLink(10, 0, null, sortOrder);
  164 + }
  165 + this.dataSource = new EntitiesDataSource<BaseData<HasId>>(
  166 + this.entitiesTableConfig.entitiesFetchFunction
  167 + );
  168 + if (this.entitiesTableConfig.onLoadAction) {
  169 + this.entitiesTableConfig.onLoadAction(this.route);
  170 + }
  171 + if (this.entitiesTableConfig.loadDataOnInit) {
  172 + this.dataSource.loadEntities(this.pageLink);
  173 + }
  174 + }
  175 +
  176 + ngAfterViewInit() {
  177 +
  178 + fromEvent(this.searchInputField.nativeElement, 'keyup')
  179 + .pipe(
  180 + debounceTime(150),
  181 + distinctUntilChanged(),
  182 + tap(() => {
  183 + this.paginator.pageIndex = 0;
  184 + this.updateData();
  185 + })
  186 + )
  187 + .subscribe();
  188 +
  189 + this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0);
  190 +
  191 + merge(this.sort.sortChange, this.paginator.page)
  192 + .pipe(
  193 + tap(() => this.updateData())
  194 + )
  195 + .subscribe();
  196 + }
  197 +
  198 + addEnabled() {
  199 + return this.entitiesTableConfig.addEnabled;
  200 + }
  201 +
  202 + updateData(closeDetails: boolean = true) {
  203 + if (closeDetails) {
  204 + this.isDetailsOpen = false;
  205 + }
  206 + this.pageLink.page = this.paginator.pageIndex;
  207 + this.pageLink.pageSize = this.paginator.pageSize;
  208 + this.pageLink.sortOrder.property = this.sort.active;
  209 + this.pageLink.sortOrder.direction = Direction[this.sort.direction.toUpperCase()];
  210 + if (this.entitiesTableConfig.useTimePageLink) {
  211 + const timePageLink = this.pageLink as TimePageLink;
  212 + if (this.timewindow.history.timewindowMs) {
  213 + const currentTime = new Date().getTime();
  214 + timePageLink.startTime = currentTime - this.timewindow.history.timewindowMs;
  215 + timePageLink.endTime = currentTime;
  216 + } else {
  217 + timePageLink.startTime = this.timewindow.history.fixedTimewindow.startTimeMs;
  218 + timePageLink.endTime = this.timewindow.history.fixedTimewindow.endTimeMs;
  219 + }
  220 + }
  221 + this.dataSource.loadEntities(this.pageLink);
  222 + }
  223 +
  224 + onRowClick($event: Event, entity) {
  225 + if ($event) {
  226 + $event.stopPropagation();
  227 + }
  228 + if (this.dataSource.toggleCurrentEntity(entity)) {
  229 + this.isDetailsOpen = true;
  230 + } else {
  231 + this.isDetailsOpen = !this.isDetailsOpen;
  232 + }
  233 + }
  234 +
  235 + addEntity($event: Event) {
  236 + let entity$: Observable<BaseData<HasId>>;
  237 + if (this.entitiesTableConfig.addEntity) {
  238 + entity$ = this.entitiesTableConfig.addEntity();
  239 + } else {
  240 + entity$ = this.dialog.open<AddEntityDialogComponent, AddEntityDialogData<BaseData<HasId>>,
  241 + BaseData<HasId>>(AddEntityDialogComponent, {
  242 + disableClose: true,
  243 + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
  244 + data: {
  245 + entitiesTableConfig: this.entitiesTableConfig
  246 + }
  247 + }).afterClosed();
  248 + }
  249 + entity$.subscribe(
  250 + (entity) => {
  251 + if (entity) {
  252 + this.updateData();
  253 + }
  254 + }
  255 + );
  256 + }
  257 +
  258 + onEntityUpdated(entity: BaseData<HasId>) {
  259 + this.updateData(false);
  260 + }
  261 +
  262 + onEntityAction(action: EntityAction<BaseData<HasId>>) {
  263 + if (action.action === 'delete') {
  264 + this.deleteEntity(action.event, action.entity);
  265 + }
  266 + }
  267 +
  268 + deleteEntity($event: Event, entity: BaseData<HasId>) {
  269 + if ($event) {
  270 + $event.stopPropagation();
  271 + }
  272 + this.dialogService.confirm(
  273 + this.entitiesTableConfig.deleteEntityTitle(entity),
  274 + this.entitiesTableConfig.deleteEntityContent(entity),
  275 + this.translate.instant('action.no'),
  276 + this.translate.instant('action.yes'),
  277 + true
  278 + ).subscribe((result) => {
  279 + if (result) {
  280 + this.entitiesTableConfig.deleteEntity(entity.id).subscribe(
  281 + () => {
  282 + this.updateData();
  283 + }
  284 + );
  285 + }
  286 + });
  287 + }
  288 +
  289 + deleteEntities($event: Event, entities: BaseData<HasId>[]) {
  290 + if ($event) {
  291 + $event.stopPropagation();
  292 + }
  293 + this.dialogService.confirm(
  294 + this.entitiesTableConfig.deleteEntitiesTitle(entities.length),
  295 + this.entitiesTableConfig.deleteEntitiesContent(entities.length),
  296 + this.translate.instant('action.no'),
  297 + this.translate.instant('action.yes'),
  298 + true
  299 + ).subscribe((result) => {
  300 + if (result) {
  301 + const tasks: Observable<any>[] = [];
  302 + entities.forEach((entity) => {
  303 + if (this.entitiesTableConfig.deleteEnabled(entity)) {
  304 + tasks.push(this.entitiesTableConfig.deleteEntity(entity.id));
  305 + }
  306 + });
  307 + forkJoin(tasks).subscribe(
  308 + () => {
  309 + this.updateData();
  310 + }
  311 + );
  312 + }
  313 + });
  314 + }
  315 +
  316 + onTimewindowChange() {
  317 + this.updateData();
  318 + }
  319 +
  320 + enterFilterMode() {
  321 + this.textSearchMode = true;
  322 + this.pageLink.textSearch = '';
  323 + setTimeout(() => {
  324 + this.searchInputField.nativeElement.focus();
  325 + this.searchInputField.nativeElement.setSelectionRange(0, 0);
  326 + }, 10);
  327 + }
  328 +
  329 + exitFilterMode() {
  330 + this.textSearchMode = false;
  331 + this.pageLink.textSearch = null;
  332 + this.paginator.pageIndex = 0;
  333 + this.updateData();
  334 + }
  335 +
  336 + resetSortAndFilter(update: boolean = true) {
  337 + this.pageLink.textSearch = null;
  338 + if (this.entitiesTableConfig.useTimePageLink) {
  339 + this.timewindow = Timewindow.historyInterval(24 * 60 * 60 * 1000);
  340 + }
  341 + this.paginator.pageIndex = 0;
  342 + const sortable = this.sort.sortables.get(this.entitiesTableConfig.defaultSortOrder.property);
  343 + this.sort.active = sortable.id;
  344 + this.sort.direction = this.entitiesTableConfig.defaultSortOrder.direction === Direction.ASC ? 'asc' : 'desc';
  345 + if (update) {
  346 + this.updateData();
  347 + }
  348 + }
  349 +
  350 + cellContent(entity: BaseData<HasId>, column: EntityTableColumn<BaseData<HasId>>) {
  351 + return this.domSanitizer.bypassSecurityTrustHtml(column.cellContentFunction(entity, column.key));
  352 + }
  353 +
  354 + cellStyle(entity: BaseData<HasId>, column: EntityTableColumn<BaseData<HasId>>) {
  355 + return {...column.cellStyleFunction(entity, column.key), ...{maxWidth: column.maxWidth}};
  356 + }
  357 +
  358 +}
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { BaseData, HasId } from '@shared/models/base-data';
  18 +import { EntityTableConfig } from '@shared/components/entity/entities-table-config.models';
  19 +
  20 +export interface AddEntityDialogData<T extends BaseData<HasId>> {
  21 + entitiesTableConfig: EntityTableConfig<T>;
  22 +}
  23 +
  24 +export interface EntityAction<T extends BaseData<HasId>> {
  25 + event: Event;
  26 + action: string;
  27 + entity: T;
  28 +}
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2019 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<tb-details-panel fxFlex
  19 + [headerTitle]="entity?.name"
  20 + headerSubtitle="{{ translations.details | translate }}"
  21 + [isReadOnly]="entitiesTableConfig.detailsReadonly(entity)"
  22 + [isEdit]="isEditValue"
  23 + (closeDetails)="onCloseEntityDetails()"
  24 + (toggleDetailsEditMode)="onToggleEditMode($event)"
  25 + (applyDetails)="saveEntity()"
  26 + [theForm]="detailsForm">
  27 + <div class="details-buttons">
  28 + <div [tb-help]="resources.helpLinkId"></div>
  29 + </div>
  30 + <mat-tab-group class="tb-absolute-fill" [ngClass]="{'tb-headless': isEditValue}" fxFlex [(selectedIndex)]="selectedTab">
  31 + <mat-tab label="{{ 'details.details' | translate }}">
  32 + <tb-anchor #entityDetailsForm></tb-anchor>
  33 + </mat-tab>
  34 + <!--mat-tab *ngIf="entity && entitiesTableConfig.entityType !== entityTypes.CUSTOMER"
  35 + label="{{ 'audit-log.audit-logs' | translate }}">
  36 + <tb-audit-log-table [active]="selectedTab === 1" [auditLogMode]="auditLogModes.ENTITY" [entityId]="entity.id" detailsMode="true"></tb-audit-log-table>
  37 + </mat-tab>
  38 + <mat-tab *ngIf="entity && entitiesTableConfig.entityType === entityTypes.CUSTOMER"
  39 + label="{{ 'audit-log.audit-logs' | translate }}">
  40 + <tb-audit-log-table [active]="selectedTab === 1" [auditLogMode]="auditLogModes.CUSTOMER" [customerId]="entity.id" detailsMode="true"></tb-audit-log-table>
  41 + </mat-tab-->
  42 + </mat-tab-group>
  43 +</tb-details-panel>
... ...
  1 +/**
  2 + * Copyright © 2016-2019 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +:host {
  17 + width: 100%;
  18 + height: 100%;
  19 + display: flex;
  20 + flex-direction: column;
  21 +}
  22 +
  23 +:host ::ng-deep {
  24 + .mat-tab-body-wrapper {
  25 + position: absolute;
  26 + top: 49px;
  27 + left: 0;
  28 + right: 0;
  29 + bottom: 0;
  30 + }
  31 +}
... ...