Commit 08e8c92970519a41d673f38f0bd7ffaa6ff470da

Authored by Igor Kulikov
1 parent 1e7f197c

Tenants and tenant admins pages.

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