Commit 9a9373791dfa3f43177a011b78b1b032e97390e9

Authored by Igor Kulikov
1 parent e91fd302

Attributes table

  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 { forkJoin, Observable, of } from 'rxjs/index';
  20 +import { HttpClient } from '@angular/common/http';
  21 +import { EntityId } from '@shared/models/id/entity-id';
  22 +import { AttributeData, AttributeScope } from '@shared/models/telemetry/telemetry.models';
  23 +
  24 +@Injectable({
  25 + providedIn: 'root'
  26 +})
  27 +export class AttributeService {
  28 +
  29 + constructor(
  30 + private http: HttpClient
  31 + ) { }
  32 +
  33 + public getEntityAttributes(entityId: EntityId, attributeScope: AttributeScope,
  34 + ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Array<AttributeData>> {
  35 + return this.http.get<Array<AttributeData>>(`/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/values/attributes/` +
  36 + `${attributeScope}`,
  37 + defaultHttpOptions(ignoreLoading, ignoreErrors));
  38 + }
  39 +
  40 + public deleteEntityAttributes(entityId: EntityId, attributeScope: AttributeScope, attributes: Array<AttributeData>,
  41 + ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<any> {
  42 + const keys = attributes.map(attribute => attribute.key).join(',');
  43 + return this.http.delete(`/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/${attributeScope}` +
  44 + `?keys=${keys}`,
  45 + defaultHttpOptions(ignoreLoading, ignoreErrors));
  46 + }
  47 +
  48 + public saveEntityAttributes(entityId: EntityId, attributeScope: AttributeScope, attributes: Array<AttributeData>,
  49 + ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<any> {
  50 + const attributesData: {[key: string]: any} = {};
  51 + const deleteAttributes: AttributeData[] = [];
  52 + attributes.forEach((attribute) => {
  53 + if (attribute.value !== null) {
  54 + attributesData[attribute.key] = attribute.value;
  55 + } else {
  56 + deleteAttributes.push(attribute);
  57 + }
  58 + });
  59 + let deleteEntityAttributesObservable: Observable<any>;
  60 + if (deleteAttributes.length) {
  61 + deleteEntityAttributesObservable = this.deleteEntityAttributes(entityId, attributeScope, deleteAttributes);
  62 + } else {
  63 + deleteEntityAttributesObservable = of(null);
  64 + }
  65 + let saveEntityAttributesObservable: Observable<any>;
  66 + if (Object.keys(attributesData).length) {
  67 + saveEntityAttributesObservable = this.http.post(`/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/${attributeScope}`,
  68 + attributesData, defaultHttpOptions(ignoreLoading, ignoreErrors));
  69 + } else {
  70 + saveEntityAttributesObservable = of(null);
  71 + }
  72 + return forkJoin(saveEntityAttributesObservable, deleteEntityAttributesObservable);
  73 + }
  74 +}
... ...
  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 #attributeForm="ngForm" [formGroup]="attributeFormGroup" (ngSubmit)="add()" style="min-width: 400px;">
  19 + <mat-toolbar fxLayout="row" color="primary">
  20 + <h2>{{ 'attribute.add' | translate }}</h2>
  21 + <span fxFlex></span>
  22 + <button mat-button mat-icon-button
  23 + (click)="cancel()"
  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 + <fieldset [disabled]="isLoading$ | async">
  33 + <mat-form-field class="mat-block">
  34 + <mat-label translate>attribute.key</mat-label>
  35 + <input matInput formControlName="key" required>
  36 + <mat-error *ngIf="attributeFormGroup.get('key').hasError('required')">
  37 + {{ 'attribute.key-required' | translate }}
  38 + </mat-error>
  39 + </mat-form-field>
  40 + <tb-value-input
  41 + formControlName="value"
  42 + requiredText="attribute.value-required">
  43 + </tb-value-input>
  44 + </fieldset>
  45 + </div>
  46 + <div mat-dialog-actions fxLayout="row">
  47 + <span fxFlex></span>
  48 + <button mat-button mat-raised-button color="primary"
  49 + type="submit"
  50 + [disabled]="(isLoading$ | async) || attributeFormGroup.invalid || !attributeFormGroup.dirty">
  51 + {{ 'action.add' | translate }}
  52 + </button>
  53 + <button mat-button color="primary"
  54 + style="margin-right: 20px;"
  55 + type="button"
  56 + [disabled]="(isLoading$ | async)"
  57 + (click)="cancel()" cdkFocusInitial>
  58 + {{ 'action.cancel' | translate }}
  59 + </button>
  60 + </div>
  61 +</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, SkipSelf, ViewChild } from '@angular/core';
  18 +import { ErrorStateMatcher, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
  19 +import { Store } from '@ngrx/store';
  20 +import { AppState } from '@core/core.state';
  21 +import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms';
  22 +import {
  23 + CONTAINS_TYPE,
  24 + EntityRelation,
  25 + EntitySearchDirection,
  26 + RelationTypeGroup
  27 +} from '@shared/models/relation.models';
  28 +import { EntityRelationService } from '@core/http/entity-relation.service';
  29 +import { EntityId } from '@shared/models/id/entity-id';
  30 +import { forkJoin, Observable } from 'rxjs';
  31 +import { JsonObjectEditComponent } from '@app/shared/components/json-object-edit.component';
  32 +import { Router } from '@angular/router';
  33 +import { DialogComponent } from '@app/shared/components/dialog.component';
  34 +import { AttributeData, AttributeScope } from '@shared/models/telemetry/telemetry.models';
  35 +import { AttributeService } from '@core/http/attribute.service';
  36 +
  37 +export interface AddAttributeDialogData {
  38 + entityId: EntityId;
  39 + attributeScope: AttributeScope;
  40 +}
  41 +
  42 +@Component({
  43 + selector: 'tb-add-attribute-dialog',
  44 + templateUrl: './add-attribute-dialog.component.html',
  45 + providers: [{provide: ErrorStateMatcher, useExisting: AddAttributeDialogComponent}],
  46 + styleUrls: []
  47 +})
  48 +export class AddAttributeDialogComponent extends DialogComponent<AddAttributeDialogComponent, boolean>
  49 + implements OnInit, ErrorStateMatcher {
  50 +
  51 + attributeFormGroup: FormGroup;
  52 +
  53 + submitted = false;
  54 +
  55 + constructor(protected store: Store<AppState>,
  56 + protected router: Router,
  57 + @Inject(MAT_DIALOG_DATA) public data: AddAttributeDialogData,
  58 + private attributeService: AttributeService,
  59 + @SkipSelf() private errorStateMatcher: ErrorStateMatcher,
  60 + public dialogRef: MatDialogRef<AddAttributeDialogComponent, boolean>,
  61 + public fb: FormBuilder) {
  62 + super(store, router, dialogRef);
  63 + }
  64 +
  65 + ngOnInit(): void {
  66 + this.attributeFormGroup = this.fb.group({
  67 + key: ['', [Validators.required]],
  68 + value: [null, [Validators.required]]
  69 + });
  70 + }
  71 +
  72 + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
  73 + const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
  74 + const customErrorState = !!(control && control.invalid && this.submitted);
  75 + return originalErrorState || customErrorState;
  76 + }
  77 +
  78 + cancel(): void {
  79 + this.dialogRef.close(false);
  80 + }
  81 +
  82 + add(): void {
  83 + this.submitted = true;
  84 + const attribute: AttributeData = {
  85 + lastUpdateTs: null,
  86 + key: this.attributeFormGroup.get('key').value,
  87 + value: this.attributeFormGroup.get('value').value
  88 + };
  89 + this.attributeService.saveEntityAttributes(this.data.entityId,
  90 + this.data.attributeScope, [attribute]).subscribe(
  91 + () => {
  92 + this.dialogRef.close(true);
  93 + }
  94 + );
  95 + }
  96 +}
... ...
  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="mat-padding tb-entity-table tb-absolute-fill">
  19 + <div fxFlex fxLayout="column" class="mat-elevation-z1 tb-entity-table-content">
  20 + <mat-toolbar class="mat-table-toolbar" [fxShow]="mode === 'default' && !textSearchMode && dataSource.selection.isEmpty()">
  21 + <div class="mat-toolbar-tools">
  22 + <span class="tb-entity-table-title">{{telemetryTypeTranslationsMap.get(attributeScope) | translate}}</span>
  23 + <mat-form-field class="mat-block tb-attribute-scope" style="width: 200px;" *ngIf="!disableAttributeScopeSelection">
  24 + <mat-label translate>attribute.attributes-scope</mat-label>
  25 + <mat-select [disabled]="(isLoading$ | async) || attributeScopeSelectionReadonly"
  26 + matInput [ngModel]="attributeScope"
  27 + (ngModelChange)="attributeScopeChanged($event)">
  28 + <mat-option *ngFor="let scope of attributeScopes" [value]="scope">
  29 + {{ telemetryTypeTranslationsMap.get(scope) | translate }}
  30 + </mat-option>
  31 + </mat-select>
  32 + </mat-form-field>
  33 + <span fxFlex></span>
  34 + <button *ngIf="!isClientSideTelemetryTypeMap.get(attributeScope)"
  35 + mat-button mat-icon-button
  36 + [disabled]="isLoading$ | async"
  37 + (click)="addAttribute($event)"
  38 + matTooltip="{{ 'action.add' | translate }}"
  39 + matTooltipPosition="above">
  40 + <mat-icon>add</mat-icon>
  41 + </button>
  42 + <button *ngIf="!isClientSideTelemetryTypeMap.get(attributeScope)"
  43 + mat-button mat-icon-button
  44 + [disabled]="isLoading$ | async"
  45 + (click)="reloadAttributes()"
  46 + matTooltip="{{ 'action.refresh' | translate }}"
  47 + matTooltipPosition="above">
  48 + <mat-icon>refresh</mat-icon>
  49 + </button>
  50 + <button mat-button mat-icon-button
  51 + [disabled]="isLoading$ | async"
  52 + (click)="enterFilterMode()"
  53 + matTooltip="{{ 'action.search' | translate }}"
  54 + matTooltipPosition="above">
  55 + <mat-icon>search</mat-icon>
  56 + </button>
  57 + </div>
  58 + </mat-toolbar>
  59 + <mat-toolbar class="mat-table-toolbar" [fxShow]="mode === 'default' && textSearchMode && dataSource.selection.isEmpty()">
  60 + <div class="mat-toolbar-tools">
  61 + <button mat-button mat-icon-button
  62 + matTooltip="{{ 'action.search' | translate }}"
  63 + matTooltipPosition="above">
  64 + <mat-icon>search</mat-icon>
  65 + </button>
  66 + <mat-form-field fxFlex>
  67 + <mat-label>&nbsp;</mat-label>
  68 + <input #searchInput matInput
  69 + [(ngModel)]="pageLink.textSearch"
  70 + placeholder="{{ 'common.enter-search' | translate }}"/>
  71 + </mat-form-field>
  72 + <button mat-button mat-icon-button (click)="exitFilterMode()"
  73 + matTooltip="{{ 'action.close' | translate }}"
  74 + matTooltipPosition="above">
  75 + <mat-icon>close</mat-icon>
  76 + </button>
  77 + </div>
  78 + </mat-toolbar>
  79 + <mat-toolbar class="mat-table-toolbar" color="primary" [fxShow]="mode === 'default' && !dataSource.selection.isEmpty()">
  80 + <div class="mat-toolbar-tools">
  81 + <span>
  82 + {{ translate.get(
  83 + attributeScope === latestTelemetryTypes.LATEST_TELEMETRY
  84 + ? 'attribute.selected-telemetry' : 'attribute.selected-attributes',
  85 + {count: dataSource.selection.selected.length}) | async }}
  86 + </span>
  87 + <span fxFlex></span>
  88 + <button mat-button mat-icon-button [disabled]="isLoading$ | async"
  89 + matTooltip="{{ 'action.delete' | translate }}"
  90 + matTooltipPosition="above"
  91 + (click)="deleteAttributes($event)">
  92 + <mat-icon>delete</mat-icon>
  93 + </button>
  94 + <button mat-button mat-raised-button
  95 + color="accent"
  96 + [disabled]="isLoading$ | async"
  97 + matTooltip="{{ 'attribute.show-on-widget' | translate }}"
  98 + matTooltipPosition="above"
  99 + (click)="enterWidgetMode()">
  100 + <mat-icon>now_widgets</mat-icon>
  101 + <span translate>attribute.show-on-widget</span>
  102 + </button>
  103 + </div>
  104 + </mat-toolbar>
  105 + <mat-toolbar class="mat-table-toolbar" color="primary" [fxShow]="mode === 'widget'">
  106 + <div class="mat-toolbar-tools">
  107 + <div fxFlex fxLayout="row" fxLayoutAlign="start">
  108 + <span class="tb-details-subtitle">{{ 'widgets-bundle.current' | translate }}</span>
  109 + <span fxFlex>TODO:</span>
  110 + </div>
  111 + <!--button mat-button mat-raised-button
  112 + color="accent"
  113 + [disabled]="isLoading$ | async"
  114 + matTooltip="{{ 'attribute.show-on-widget' | translate }}"
  115 + matTooltipPosition="above"
  116 + (click)="enterWidgetMode()">
  117 + <mat-icon>now_widgets</mat-icon>
  118 + <span translate>attribute.show-on-widget</span>
  119 + </button-->
  120 + <button mat-button mat-icon-button
  121 + [disabled]="isLoading$ | async"
  122 + matTooltip="{{ 'action.close' | translate }}"
  123 + matTooltipPosition="above"
  124 + (click)="exitWidgetMode()">
  125 + <mat-icon>close</mat-icon>
  126 + </button>
  127 + </div>
  128 + </mat-toolbar>
  129 + <div fxFlex class="table-container" [fxShow]="mode !== 'widget'">
  130 + <mat-table [dataSource]="dataSource"
  131 + matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="(pageLink.sortOrder.direction + '').toLowerCase()" matSortDisableClear>
  132 + <ng-container matColumnDef="select" sticky>
  133 + <mat-header-cell *matHeaderCellDef>
  134 + <mat-checkbox (change)="$event ? dataSource.masterToggle() : null"
  135 + [checked]="dataSource.selection.hasValue() && (dataSource.isAllSelected() | async)"
  136 + [indeterminate]="dataSource.selection.hasValue() && !(dataSource.isAllSelected() | async)">
  137 + </mat-checkbox>
  138 + </mat-header-cell>
  139 + <mat-cell *matCellDef="let attribute">
  140 + <mat-checkbox (click)="$event.stopPropagation()"
  141 + (change)="$event ? dataSource.selection.toggle(attribute) : null"
  142 + [checked]="dataSource.selection.isSelected(attribute)">
  143 + </mat-checkbox>
  144 + </mat-cell>
  145 + </ng-container>
  146 + <ng-container matColumnDef="lastUpdateTs">
  147 + <mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'attribute.last-update-time' | translate }} </mat-header-cell>
  148 + <mat-cell *matCellDef="let attribute">
  149 + {{ attribute.lastUpdateTs | date:'yyyy-MM-dd HH:mm:ss' }}
  150 + </mat-cell>
  151 + </ng-container>
  152 + <ng-container matColumnDef="key">
  153 + <mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'attribute.key' | translate }} </mat-header-cell>
  154 + <mat-cell *matCellDef="let attribute">
  155 + {{ attribute.key }}
  156 + </mat-cell>
  157 + </ng-container>
  158 + <ng-container matColumnDef="value">
  159 + <mat-header-cell *matHeaderCellDef> {{ 'attribute.value' | translate }} </mat-header-cell>
  160 + <mat-cell *matCellDef="let attribute"
  161 + class="tb-value-cell"
  162 + (click)="editAttribute($event, attribute)">
  163 + <span fxFlex>{{attribute.value}}</span>
  164 + <span [fxShow]="!isClientSideTelemetryTypeMap.get(attributeScope)">
  165 + <mat-icon>edit</mat-icon>
  166 + </span>
  167 + </mat-cell>
  168 + </ng-container>
  169 + <mat-header-row [ngClass]="{'mat-row-select': true}" *matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row>
  170 + <mat-row [ngClass]="{'mat-row-select': true,
  171 + 'mat-selected': dataSource.selection.isSelected(attribute)}"
  172 + *matRowDef="let attribute; columns: displayedColumns;" (click)="dataSource.selection.toggle(attribute)"></mat-row>
  173 + </mat-table>
  174 + <span [fxShow]="dataSource.isEmpty() | async"
  175 + fxLayoutAlign="center center"
  176 + class="no-data-found" translate>{{
  177 + attributeScope === latestTelemetryTypes.LATEST_TELEMETRY
  178 + ? 'attribute.no-telemetry-text'
  179 + : 'attribute.no-attributes-text'
  180 + }}</span>
  181 + </div>
  182 + <mat-divider [fxShow]="mode !== 'widget'"></mat-divider>
  183 + <mat-paginator [fxShow]="mode !== 'widget'"
  184 + [length]="dataSource.total() | async"
  185 + [pageIndex]="pageLink.page"
  186 + [pageSize]="pageLink.pageSize"
  187 + [pageSizeOptions]="[10, 20, 30]"></mat-paginator>
  188 + <div fxFlex [fxShow]="mode === 'widget'">
  189 + Coming soon!
  190 + </div>
  191 + </div>
  192 +</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 + 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 {
  40 + .mat-sort-header-sorted .mat-sort-header-arrow {
  41 + opacity: 1 !important;
  42 + }
  43 + mat-form-field.tb-attribute-scope {
  44 + font-size: 16px;
  45 +
  46 + .mat-form-field-wrapper {
  47 + padding-bottom: 0;
  48 + }
  49 +
  50 + .mat-form-field-underline {
  51 + bottom: 0;
  52 + }
  53 + }
  54 + mat-cell.tb-value-cell {
  55 + cursor: pointer;
  56 + mat-icon {
  57 + height: 16px;
  58 + width: 16px;
  59 + font-size: 16px;
  60 + color: #757575
  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 + AfterViewInit,
  19 + ChangeDetectionStrategy,
  20 + Component,
  21 + ElementRef,
  22 + Input,
  23 + OnInit,
  24 + ViewChild,
  25 + ViewContainerRef
  26 +} from '@angular/core';
  27 +import { PageComponent } from '@shared/components/page.component';
  28 +import { PageLink } from '@shared/models/page/page-link';
  29 +import { MatPaginator } from '@angular/material/paginator';
  30 +import { MatSort } from '@angular/material/sort';
  31 +import { Store } from '@ngrx/store';
  32 +import { AppState } from '@core/core.state';
  33 +import { TranslateService } from '@ngx-translate/core';
  34 +import { MatDialog } from '@angular/material/dialog';
  35 +import { DialogService } from '@core/services/dialog.service';
  36 +import { Direction, SortOrder } from '@shared/models/page/sort-order';
  37 +import { fromEvent, merge } from 'rxjs';
  38 +import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators';
  39 +import { EntityId } from '@shared/models/id/entity-id';
  40 +import {
  41 + AttributeData,
  42 + AttributeScope,
  43 + isClientSideTelemetryType, LatestTelemetry,
  44 + TelemetryType,
  45 + telemetryTypeTranslations
  46 +} from '@shared/models/telemetry/telemetry.models';
  47 +import { AttributeDatasource } from '@home/models/datasource/attribute-datasource';
  48 +import { AttributeService } from '@app/core/http/attribute.service';
  49 +import { EntityType } from '@shared/models/entity-type.models';
  50 +import { coerceBooleanProperty } from '@angular/cdk/coercion';
  51 +import { RelationDialogComponent, RelationDialogData } from '@home/components/relation/relation-dialog.component';
  52 +import {
  53 + AddAttributeDialogComponent,
  54 + AddAttributeDialogData
  55 +} from '@home/components/attribute/add-attribute-dialog.component';
  56 +import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
  57 +import { TIMEWINDOW_PANEL_DATA, TimewindowPanelComponent } from '@shared/components/time/timewindow-panel.component';
  58 +import {
  59 + EDIT_ATTRIBUTE_VALUE_PANEL_DATA,
  60 + EditAttributeValuePanelComponent,
  61 + EditAttributeValuePanelData
  62 +} from './edit-attribute-value-panel.component';
  63 +import { ComponentPortal, PortalInjector } from '@angular/cdk/portal';
  64 +
  65 +
  66 +@Component({
  67 + selector: 'tb-attribute-table',
  68 + templateUrl: './attribute-table.component.html',
  69 + styleUrls: ['./attribute-table.component.scss'],
  70 + changeDetection: ChangeDetectionStrategy.OnPush
  71 +})
  72 +export class AttributeTableComponent extends PageComponent implements AfterViewInit, OnInit {
  73 +
  74 + telemetryTypeTranslationsMap = telemetryTypeTranslations;
  75 + isClientSideTelemetryTypeMap = isClientSideTelemetryType;
  76 +
  77 + latestTelemetryTypes = LatestTelemetry;
  78 +
  79 + mode: 'default' | 'widget' = 'default';
  80 +
  81 + attributeScopes: Array<string> = [];
  82 + attributeScope: TelemetryType;
  83 +
  84 + displayedColumns = ['select', 'lastUpdateTs', 'key', 'value'];
  85 + pageLink: PageLink;
  86 + textSearchMode = false;
  87 + dataSource: AttributeDatasource;
  88 +
  89 + activeValue = false;
  90 + dirtyValue = false;
  91 + entityIdValue: EntityId;
  92 +
  93 + attributeScopeSelectionReadonly = false;
  94 +
  95 + viewsInited = false;
  96 +
  97 + private disableAttributeScopeSelectionValue: boolean;
  98 + get disableAttributeScopeSelection(): boolean {
  99 + return this.disableAttributeScopeSelectionValue;
  100 + }
  101 + @Input()
  102 + set disableAttributeScopeSelection(value: boolean) {
  103 + this.disableAttributeScopeSelectionValue = coerceBooleanProperty(value);
  104 + }
  105 +
  106 + @Input()
  107 + defaultAttributeScope: TelemetryType;
  108 +
  109 + @Input()
  110 + set active(active: boolean) {
  111 + if (this.activeValue !== active) {
  112 + this.activeValue = active;
  113 + if (this.activeValue && this.dirtyValue) {
  114 + this.dirtyValue = false;
  115 + if (this.viewsInited) {
  116 + this.updateData(true);
  117 + }
  118 + }
  119 + }
  120 + }
  121 +
  122 + @Input()
  123 + set entityId(entityId: EntityId) {
  124 + if (this.entityIdValue !== entityId) {
  125 + this.entityIdValue = entityId;
  126 + this.resetSortAndFilter(this.activeValue);
  127 + if (!this.activeValue) {
  128 + this.dirtyValue = true;
  129 + }
  130 + }
  131 + }
  132 +
  133 + @ViewChild('searchInput', {static: false}) searchInputField: ElementRef;
  134 +
  135 + @ViewChild(MatPaginator, {static: false}) paginator: MatPaginator;
  136 + @ViewChild(MatSort, {static: false}) sort: MatSort;
  137 +
  138 + constructor(protected store: Store<AppState>,
  139 + private attributeService: AttributeService,
  140 + public translate: TranslateService,
  141 + public dialog: MatDialog,
  142 + private overlay: Overlay,
  143 + private viewContainerRef: ViewContainerRef,
  144 + private dialogService: DialogService) {
  145 + super(store);
  146 + this.dirtyValue = !this.activeValue;
  147 + const sortOrder: SortOrder = { property: 'key', direction: Direction.ASC };
  148 + this.pageLink = new PageLink(10, 0, null, sortOrder);
  149 + this.dataSource = new AttributeDatasource(this.attributeService, this.translate);
  150 + }
  151 +
  152 + ngOnInit() {
  153 + }
  154 +
  155 + attributeScopeChanged(attributeScope: TelemetryType) {
  156 + this.attributeScope = attributeScope;
  157 + this.mode = 'default';
  158 + this.updateData(true);
  159 + }
  160 +
  161 + ngAfterViewInit() {
  162 +
  163 + fromEvent(this.searchInputField.nativeElement, 'keyup')
  164 + .pipe(
  165 + debounceTime(150),
  166 + distinctUntilChanged(),
  167 + tap(() => {
  168 + this.paginator.pageIndex = 0;
  169 + this.updateData();
  170 + })
  171 + )
  172 + .subscribe();
  173 +
  174 + this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0);
  175 +
  176 + merge(this.sort.sortChange, this.paginator.page)
  177 + .pipe(
  178 + tap(() => this.updateData())
  179 + )
  180 + .subscribe();
  181 +
  182 + this.viewsInited = true;
  183 + if (this.activeValue && this.entityIdValue) {
  184 + this.updateData(true);
  185 + }
  186 + }
  187 +
  188 + updateData(reload: boolean = false) {
  189 + this.pageLink.page = this.paginator.pageIndex;
  190 + this.pageLink.pageSize = this.paginator.pageSize;
  191 + this.pageLink.sortOrder.property = this.sort.active;
  192 + this.pageLink.sortOrder.direction = Direction[this.sort.direction.toUpperCase()];
  193 + this.dataSource.loadAttributes(this.entityIdValue, this.attributeScope, this.pageLink, reload);
  194 + }
  195 +
  196 + enterFilterMode() {
  197 + this.textSearchMode = true;
  198 + this.pageLink.textSearch = '';
  199 + setTimeout(() => {
  200 + this.searchInputField.nativeElement.focus();
  201 + this.searchInputField.nativeElement.setSelectionRange(0, 0);
  202 + }, 10);
  203 + }
  204 +
  205 + exitFilterMode() {
  206 + this.textSearchMode = false;
  207 + this.pageLink.textSearch = null;
  208 + this.paginator.pageIndex = 0;
  209 + this.updateData();
  210 + }
  211 +
  212 + resetSortAndFilter(update: boolean = true) {
  213 + const entityType = this.entityIdValue.entityType;
  214 + if (entityType === EntityType.DEVICE || entityType === EntityType.ENTITY_VIEW) {
  215 + this.attributeScopes = Object.keys(AttributeScope);
  216 + this.attributeScopeSelectionReadonly = false;
  217 + } else {
  218 + this.attributeScopes = [AttributeScope.SERVER_SCOPE];
  219 + this.attributeScopeSelectionReadonly = true;
  220 + }
  221 + this.mode = 'default';
  222 + this.attributeScope = this.defaultAttributeScope;
  223 + this.pageLink.textSearch = null;
  224 + if (this.viewsInited) {
  225 + this.paginator.pageIndex = 0;
  226 + const sortable = this.sort.sortables.get('key');
  227 + this.sort.active = sortable.id;
  228 + this.sort.direction = 'asc';
  229 + if (update) {
  230 + this.updateData(true);
  231 + }
  232 + }
  233 + }
  234 +
  235 + reloadAttributes() {
  236 + this.updateData(true);
  237 + }
  238 +
  239 + addAttribute($event: Event) {
  240 + if ($event) {
  241 + $event.stopPropagation();
  242 + }
  243 + this.dialog.open<AddAttributeDialogComponent, AddAttributeDialogData, boolean>(AddAttributeDialogComponent, {
  244 + disableClose: true,
  245 + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
  246 + data: {
  247 + entityId: this.entityIdValue,
  248 + attributeScope: this.attributeScope as AttributeScope
  249 + }
  250 + }).afterClosed().subscribe(
  251 + (res) => {
  252 + if (res) {
  253 + this.reloadAttributes();
  254 + }
  255 + }
  256 + );
  257 + }
  258 +
  259 + editAttribute($event: Event, attribute: AttributeData) {
  260 + if ($event) {
  261 + $event.stopPropagation();
  262 + }
  263 + const target = $event.target || $event.srcElement || $event.currentTarget;
  264 + const config = new OverlayConfig();
  265 + config.backdropClass = 'cdk-overlay-transparent-backdrop';
  266 + config.hasBackdrop = true;
  267 + const connectedPosition: ConnectedPosition = {
  268 + originX: 'end',
  269 + originY: 'center',
  270 + overlayX: 'end',
  271 + overlayY: 'center'
  272 + };
  273 + config.positionStrategy = this.overlay.position().flexibleConnectedTo(target as HTMLElement)
  274 + .withPositions([connectedPosition]);
  275 +
  276 + const overlayRef = this.overlay.create(config);
  277 + overlayRef.backdropClick().subscribe(() => {
  278 + overlayRef.dispose();
  279 + });
  280 + const injectionTokens = new WeakMap<any, any>([
  281 + [EDIT_ATTRIBUTE_VALUE_PANEL_DATA, {
  282 + attributeValue: attribute.value
  283 + } as EditAttributeValuePanelData],
  284 + [OverlayRef, overlayRef]
  285 + ]);
  286 + const injector = new PortalInjector(this.viewContainerRef.injector, injectionTokens);
  287 + const componentRef = overlayRef.attach(new ComponentPortal(EditAttributeValuePanelComponent,
  288 + this.viewContainerRef, injector));
  289 + componentRef.onDestroy(() => {
  290 + if (componentRef.instance.result !== null) {
  291 + const attributeValue = componentRef.instance.result;
  292 + const updatedAttribute = {...attribute};
  293 + updatedAttribute.value = attributeValue;
  294 + this.attributeService.saveEntityAttributes(this.entityIdValue,
  295 + this.attributeScope as AttributeScope, [updatedAttribute]).subscribe(
  296 + () => {
  297 + this.reloadAttributes();
  298 + }
  299 + );
  300 + }
  301 + });
  302 + }
  303 +
  304 + deleteAttributes($event: Event) {
  305 + if ($event) {
  306 + $event.stopPropagation();
  307 + }
  308 + if (this.dataSource.selection.selected.length > 0) {
  309 + this.dialogService.confirm(
  310 + this.translate.instant('attribute.delete-attributes-title', {count: this.dataSource.selection.selected.length}),
  311 + this.translate.instant('attribute.delete-attributes-text'),
  312 + this.translate.instant('action.no'),
  313 + this.translate.instant('action.yes'),
  314 + true
  315 + ).subscribe((result) => {
  316 + if (result) {
  317 + this.attributeService.deleteEntityAttributes(this.entityIdValue,
  318 + this.attributeScope as AttributeScope, this.dataSource.selection.selected).subscribe(
  319 + () => {
  320 + this.reloadAttributes();
  321 + }
  322 + );
  323 + }
  324 + });
  325 + }
  326 + }
  327 +
  328 + enterWidgetMode() {
  329 + this.mode = 'widget';
  330 +
  331 + // TODO:
  332 + }
  333 +
  334 + exitWidgetMode() {
  335 + this.mode = 'default';
  336 + this.reloadAttributes();
  337 +
  338 + // TODO:
  339 + }
  340 +
  341 +}
... ...
  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 #attributeForm="ngForm"
  19 + class="mat-elevation-z1"
  20 + [formGroup]="attributeFormGroup" (ngSubmit)="update()" style="width: 400px; padding: 5px;">
  21 + <fieldset [disabled]="isLoading$ | async">
  22 + <tb-value-input
  23 + formControlName="value"
  24 + requiredText="attribute.value-required">
  25 + </tb-value-input>
  26 + </fieldset>
  27 + <div fxLayout="row" class="tb-panel-actions">
  28 + <span fxFlex></span>
  29 + <button mat-button color="primary"
  30 + style="margin-right: 20px;"
  31 + type="button"
  32 + [disabled]="(isLoading$ | async)"
  33 + (click)="cancel()" cdkFocusInitial>
  34 + {{ 'action.cancel' | translate }}
  35 + </button>
  36 + <button mat-button mat-raised-button color="primary"
  37 + type="submit"
  38 + [disabled]="(isLoading$ | async) || attributeFormGroup.invalid || !attributeFormGroup.dirty">
  39 + {{ 'action.update' | translate }}
  40 + </button>
  41 + </div>
  42 +</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 + background-color: #f9f9f9;
  18 +}
  19 +
... ...
  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, InjectionToken, OnInit, SkipSelf, ViewChild } from '@angular/core';
  18 +import { ErrorStateMatcher, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
  19 +import { Store } from '@ngrx/store';
  20 +import { AppState } from '@core/core.state';
  21 +import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms';
  22 +import {
  23 + CONTAINS_TYPE,
  24 + EntityRelation,
  25 + EntitySearchDirection,
  26 + RelationTypeGroup
  27 +} from '@shared/models/relation.models';
  28 +import { EntityRelationService } from '@core/http/entity-relation.service';
  29 +import { EntityId } from '@shared/models/id/entity-id';
  30 +import { forkJoin, Observable } from 'rxjs';
  31 +import { JsonObjectEditComponent } from '@app/shared/components/json-object-edit.component';
  32 +import { Router } from '@angular/router';
  33 +import { DialogComponent } from '@app/shared/components/dialog.component';
  34 +import { AttributeData, AttributeScope } from '@shared/models/telemetry/telemetry.models';
  35 +import { AttributeService } from '@core/http/attribute.service';
  36 +import { PageComponent } from '@shared/components/page.component';
  37 +import { OverlayRef } from '@angular/cdk/overlay';
  38 +
  39 +export const EDIT_ATTRIBUTE_VALUE_PANEL_DATA = new InjectionToken<any>('EditAttributeValuePanelData');
  40 +
  41 +export interface EditAttributeValuePanelData {
  42 + attributeValue: any;
  43 +}
  44 +
  45 +@Component({
  46 + selector: 'tb-edit-attribute-value-panel',
  47 + templateUrl: './edit-attribute-value-panel.component.html',
  48 + providers: [{provide: ErrorStateMatcher, useExisting: EditAttributeValuePanelComponent}],
  49 + styleUrls: ['./edit-attribute-value-panel.component.scss']
  50 +})
  51 +export class EditAttributeValuePanelComponent extends PageComponent implements OnInit, ErrorStateMatcher {
  52 +
  53 + attributeFormGroup: FormGroup;
  54 +
  55 + result: any = null;
  56 +
  57 + submitted = false;
  58 +
  59 + constructor(protected store: Store<AppState>,
  60 + @Inject(EDIT_ATTRIBUTE_VALUE_PANEL_DATA) public data: EditAttributeValuePanelData,
  61 + @SkipSelf() private errorStateMatcher: ErrorStateMatcher,
  62 + public overlayRef: OverlayRef,
  63 + public fb: FormBuilder) {
  64 + super(store);
  65 + }
  66 +
  67 + ngOnInit(): void {
  68 + this.attributeFormGroup = this.fb.group({
  69 + value: [this.data.attributeValue, [Validators.required]]
  70 + });
  71 + }
  72 +
  73 + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
  74 + const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
  75 + const customErrorState = !!(control && control.invalid && this.submitted);
  76 + return originalErrorState || customErrorState;
  77 + }
  78 +
  79 + cancel(): void {
  80 + this.overlayRef.dispose();
  81 + }
  82 +
  83 + update(): void {
  84 + this.submitted = true;
  85 + this.result = this.attributeFormGroup.get('value').value;
  86 + this.overlayRef.dispose();
  87 + }
  88 +}
... ...
... ... @@ -28,4 +28,7 @@
28 28 right: 0;
29 29 bottom: 0;
30 30 }
  31 + .mat-tab-label {
  32 + min-width: 40px;
  33 + }
31 34 }
... ...
... ... @@ -38,10 +38,14 @@ import { AuthUser } from '@shared/models/user.model';
38 38 import { EntityType } from '@shared/models/entity-type.models';
39 39 import { AuditLogMode } from '@shared/models/audit-log.models';
40 40 import { DebugEventType, EventType } from '@shared/models/event.models';
  41 +import { AttributeScope, LatestTelemetry } from '@shared/models/telemetry/telemetry.models';
41 42 import { NULL_UUID } from '@shared/models/id/has-uuid';
42 43
43 44 export abstract class EntityTabsComponent<T extends BaseData<HasId>> extends PageComponent implements OnInit, AfterViewInit {
44 45
  46 + attributeScopes = AttributeScope;
  47 + latestTelemetryTypes = LatestTelemetry;
  48 +
45 49 authorities = Authority;
46 50
47 51 entityTypes = EntityType;
... ...
... ... @@ -31,6 +31,9 @@ import { RelationDialogComponent } from './relation/relation-dialog.component';
31 31 import { AlarmTableHeaderComponent } from '@home/components/alarm/alarm-table-header.component';
32 32 import { AlarmTableComponent } from '@home/components/alarm/alarm-table.component';
33 33 import { AlarmDetailsDialogComponent } from '@home/components/alarm/alarm-details-dialog.component';
  34 +import { AttributeTableComponent } from '@home/components/attribute/attribute-table.component';
  35 +import { AddAttributeDialogComponent } from './attribute/add-attribute-dialog.component';
  36 +import { EditAttributeValuePanelComponent } from './attribute/edit-attribute-value-panel.component';
34 37
35 38 @NgModule({
36 39 entryComponents: [
... ... @@ -39,7 +42,9 @@ import { AlarmDetailsDialogComponent } from '@home/components/alarm/alarm-detail
39 42 EventTableHeaderComponent,
40 43 RelationDialogComponent,
41 44 AlarmTableHeaderComponent,
42   - AlarmDetailsDialogComponent
  45 + AlarmDetailsDialogComponent,
  46 + AddAttributeDialogComponent,
  47 + EditAttributeValuePanelComponent
43 48 ],
44 49 declarations:
45 50 [
... ... @@ -56,7 +61,10 @@ import { AlarmDetailsDialogComponent } from '@home/components/alarm/alarm-detail
56 61 RelationDialogComponent,
57 62 AlarmTableHeaderComponent,
58 63 AlarmTableComponent,
59   - AlarmDetailsDialogComponent
  64 + AlarmDetailsDialogComponent,
  65 + AttributeTableComponent,
  66 + AddAttributeDialogComponent,
  67 + EditAttributeValuePanelComponent
60 68 ],
61 69 imports: [
62 70 CommonModule,
... ... @@ -72,7 +80,8 @@ import { AlarmDetailsDialogComponent } from '@home/components/alarm/alarm-detail
72 80 EventTableComponent,
73 81 RelationTableComponent,
74 82 AlarmTableComponent,
75   - AlarmDetailsDialogComponent
  83 + AlarmDetailsDialogComponent,
  84 + AttributeTableComponent
76 85 ]
77 86 })
78 87 export class HomeComponentsModule { }
... ...
  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 { CollectionViewer, DataSource } from '@angular/cdk/typings/collections';
  18 +import { BehaviorSubject, Observable, of, ReplaySubject } from 'rxjs';
  19 +import { emptyPageData, PageData } from '@shared/models/page/page-data';
  20 +import { SelectionModel } from '@angular/cdk/collections';
  21 +import { PageLink } from '@shared/models/page/page-link';
  22 +import { catchError, map, publishReplay, refCount, take, tap } from 'rxjs/operators';
  23 +import { EntityId } from '@app/shared/models/id/entity-id';
  24 +import { TranslateService } from '@ngx-translate/core';
  25 +import {
  26 + AttributeData,
  27 + AttributeScope,
  28 + isClientSideTelemetryType,
  29 + TelemetryType
  30 +} from '@shared/models/telemetry/telemetry.models';
  31 +import { AttributeService } from '@core/http/attribute.service';
  32 +
  33 +export class AttributeDatasource implements DataSource<AttributeData> {
  34 +
  35 + private attributesSubject = new BehaviorSubject<AttributeData[]>([]);
  36 + private pageDataSubject = new BehaviorSubject<PageData<AttributeData>>(emptyPageData<AttributeData>());
  37 +
  38 + public pageData$ = this.pageDataSubject.asObservable();
  39 +
  40 + public selection = new SelectionModel<AttributeData>(true, []);
  41 +
  42 + private allAttributes: Observable<Array<AttributeData>>;
  43 +
  44 + constructor(private attributeService: AttributeService,
  45 + private translate: TranslateService) {}
  46 +
  47 + connect(collectionViewer: CollectionViewer): Observable<AttributeData[] | ReadonlyArray<AttributeData>> {
  48 + return this.attributesSubject.asObservable();
  49 + }
  50 +
  51 + disconnect(collectionViewer: CollectionViewer): void {
  52 + this.attributesSubject.complete();
  53 + this.pageDataSubject.complete();
  54 + }
  55 +
  56 + loadAttributes(entityId: EntityId, attributesScope: TelemetryType,
  57 + pageLink: PageLink, reload: boolean = false): Observable<PageData<AttributeData>> {
  58 + if (reload) {
  59 + this.allAttributes = null;
  60 + }
  61 + const result = new ReplaySubject<PageData<AttributeData>>();
  62 + this.fetchAttributes(entityId, attributesScope, pageLink).pipe(
  63 + tap(() => {
  64 + this.selection.clear();
  65 + }),
  66 + catchError(() => of(emptyPageData<AttributeData>())),
  67 + ).subscribe(
  68 + (pageData) => {
  69 + this.attributesSubject.next(pageData.data);
  70 + this.pageDataSubject.next(pageData);
  71 + result.next(pageData);
  72 + }
  73 + );
  74 + return result;
  75 + }
  76 +
  77 + fetchAttributes(entityId: EntityId, attributesScope: TelemetryType,
  78 + pageLink: PageLink): Observable<PageData<AttributeData>> {
  79 + return this.getAllAttributes(entityId, attributesScope).pipe(
  80 + map((data) => pageLink.filterData(data))
  81 + );
  82 + }
  83 +
  84 + getAllAttributes(entityId: EntityId, attributesScope: TelemetryType): Observable<Array<AttributeData>> {
  85 + if (!this.allAttributes) {
  86 + let attributesObservable: Observable<Array<AttributeData>>;
  87 + if (isClientSideTelemetryType.get(attributesScope)) {
  88 + attributesObservable = of([]);
  89 + // TODO:
  90 + } else {
  91 + attributesObservable = this.attributeService.getEntityAttributes(entityId, attributesScope as AttributeScope);
  92 + }
  93 + this.allAttributes = attributesObservable.pipe(
  94 + publishReplay(1),
  95 + refCount()
  96 + );
  97 + }
  98 + return this.allAttributes;
  99 + }
  100 +
  101 + isAllSelected(): Observable<boolean> {
  102 + const numSelected = this.selection.selected.length;
  103 + return this.attributesSubject.pipe(
  104 + map((attributes) => numSelected === attributes.length)
  105 + );
  106 + }
  107 +
  108 + isEmpty(): Observable<boolean> {
  109 + return this.attributesSubject.pipe(
  110 + map((attributes) => !attributes.length)
  111 + );
  112 + }
  113 +
  114 + total(): Observable<number> {
  115 + return this.pageDataSubject.pipe(
  116 + map((pageData) => pageData.totalElements)
  117 + );
  118 + }
  119 +
  120 + masterToggle() {
  121 + this.attributesSubject.pipe(
  122 + tap((attributes) => {
  123 + const numSelected = this.selection.selected.length;
  124 + if (numSelected === attributes.length) {
  125 + this.selection.clear();
  126 + } else {
  127 + attributes.forEach(row => {
  128 + this.selection.select(row);
  129 + });
  130 + }
  131 + }),
  132 + take(1)
  133 + ).subscribe();
  134 + }
  135 +}
... ...
... ... @@ -16,6 +16,21 @@
16 16
17 17 -->
18 18 <mat-tab *ngIf="entity"
  19 + label="{{ 'attribute.attributes' | translate }}" #attributesTab="matTab">
  20 + <tb-attribute-table [active]="attributesTab.isActive"
  21 + [entityId]="entity.id"
  22 + [defaultAttributeScope]="attributeScopes.CLIENT_SCOPE">
  23 + </tb-attribute-table>
  24 +</mat-tab>
  25 +<mat-tab *ngIf="entity"
  26 + label="{{ 'attribute.latest-telemetry' | translate }}" #telemetryTab="matTab">
  27 + <tb-attribute-table [active]="telemetryTab.isActive"
  28 + [entityId]="entity.id"
  29 + [defaultAttributeScope]="latestTelemetryTypes.LATEST_TELEMETRY"
  30 + disableAttributeScopeSelection>
  31 + </tb-attribute-table>
  32 +</mat-tab>
  33 +<mat-tab *ngIf="entity"
19 34 label="{{ 'alarm.alarms' | translate }}" #alarmsTab="matTab">
20 35 <tb-alarm-table [active]="alarmsTab.isActive" [entityId]="entity.id"></tb-alarm-table>
21 36 </mat-tab>
... ...
  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 #inputForm="ngForm">
  19 + <section fxLayout="row" fxLayoutGap="8px">
  20 + <mat-form-field fxFlex="40" class="mat-block tb-value-type">
  21 + <mat-label translate>value.type</mat-label>
  22 + <mat-select [disabled]="disabled" matInput name="valueType" [(ngModel)]="valueType" (ngModelChange)="onValueTypeChanged()">
  23 + <mat-select-trigger>
  24 + <mat-icon svgIcon="{{ valueTypes.get(valueType).icon }}"></mat-icon>
  25 + <span>{{ valueTypes.get(valueType).name | translate }}</span>
  26 + </mat-select-trigger>
  27 + <mat-option *ngFor="let valueType of valueTypeKeys" [value]="valueType">
  28 + <mat-icon svgIcon="{{ valueTypes.get(valueType).icon }}"></mat-icon>
  29 + <span>{{ valueTypes.get(valueType).name | translate }}</span>
  30 + </mat-option>
  31 + </mat-select>
  32 + </mat-form-field>
  33 + <mat-form-field *ngIf="valueType === valueTypeEnum.STRING" fxFlex="60" class="mat-block">
  34 + <mat-label translate>value.string-value</mat-label>
  35 + <input [disabled]="disabled" matInput required name="value" #value="ngModel" [(ngModel)]="modelValue" (ngModelChange)="onValueChanged()"/>
  36 + <mat-error *ngIf="value.hasError('required')">
  37 + {{ (requiredText ? requiredText : 'value.string-value-required') | translate }}
  38 + </mat-error>
  39 + </mat-form-field>
  40 + <mat-form-field *ngIf="valueType === valueTypeEnum.INTEGER" fxFlex="60" class="mat-block">
  41 + <mat-label translate>value.integer-value</mat-label>
  42 + <input [disabled]="disabled" matInput required name="value" type="number" step="1" pattern="^-?[0-9]+$" #value="ngModel" [(ngModel)]="modelValue" (ngModelChange)="onValueChanged()"/>
  43 + <mat-error *ngIf="value.hasError('required')">
  44 + {{ (requiredText ? requiredText : 'value.integer-value-required') | translate }}
  45 + </mat-error>
  46 + <mat-error *ngIf="value.hasError('pattern')">
  47 + {{ 'value.invalid-integer-value' | translate }}
  48 + </mat-error>
  49 + </mat-form-field>
  50 + <mat-form-field *ngIf="valueType === valueTypeEnum.DOUBLE" fxFlex="60" class="mat-block">
  51 + <mat-label translate>value.double-value</mat-label>
  52 + <input [disabled]="disabled" matInput required name="value" type="number" step="any" #value="ngModel" [(ngModel)]="modelValue" (ngModelChange)="onValueChanged()"/>
  53 + <mat-error *ngIf="value.hasError('required')">
  54 + {{ (requiredText ? requiredText : 'value.double-value-required') | translate }}
  55 + </mat-error>
  56 + </mat-form-field>
  57 + <div fxLayout="column" fxLayoutAlign="center" fxFlex="60" *ngIf="valueType === valueTypeEnum.BOOLEAN">
  58 + <mat-checkbox [disabled]="disabled" name="value" #value="ngModel" [(ngModel)]="modelValue" (ngModelChange)="onValueChanged()" style="margin-bottom: 0px;">
  59 + {{ (modelValue ? 'value.true' : 'value.false') | translate }}
  60 + </mat-checkbox>
  61 + </div>
  62 + </section>
  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 +
  17 +:host ::ng-deep {
  18 + mat-form-field.tb-value-type {
  19 + .mat-form-field-infix {
  20 + padding-bottom: 1px;
  21 + }
  22 + mat-select-trigger {
  23 + mat-icon {
  24 + margin-right: 16px;
  25 + }
  26 + }
  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 { Component, forwardRef, Input, OnInit, ViewChild } from '@angular/core';
  18 +import { ControlValueAccessor, NG_VALUE_ACCESSOR, NgForm } from '@angular/forms';
  19 +import { ValueType, valueTypesMap } from '@shared/models/constants';
  20 +
  21 +@Component({
  22 + selector: 'tb-value-input',
  23 + templateUrl: './value-input.component.html',
  24 + styleUrls: ['./value-input.component.scss'],
  25 + providers: [
  26 + {
  27 + provide: NG_VALUE_ACCESSOR,
  28 + useExisting: forwardRef(() => ValueInputComponent),
  29 + multi: true
  30 + }
  31 + ]
  32 +})
  33 +export class ValueInputComponent implements OnInit, ControlValueAccessor {
  34 +
  35 + @Input() disabled: boolean;
  36 +
  37 + @Input() requiredText: string;
  38 +
  39 + @ViewChild('inputForm', {static: true}) inputForm: NgForm;
  40 +
  41 + modelValue: any;
  42 +
  43 + valueType: ValueType;
  44 +
  45 + public valueTypeEnum = ValueType;
  46 +
  47 + valueTypeKeys = Object.keys(ValueType);
  48 +
  49 + valueTypes = valueTypesMap;
  50 +
  51 + private propagateChange = null;
  52 +
  53 + constructor() {
  54 +
  55 + }
  56 +
  57 + ngOnInit(): void {
  58 + }
  59 +
  60 + registerOnChange(fn: any): void {
  61 + this.propagateChange = fn;
  62 + }
  63 +
  64 + registerOnTouched(fn: any): void {
  65 + }
  66 +
  67 + setDisabledState(isDisabled: boolean): void {
  68 + this.disabled = isDisabled;
  69 + }
  70 +
  71 + writeValue(value: any): void {
  72 + this.modelValue = value;
  73 + if (this.modelValue === true || this.modelValue === false) {
  74 + this.valueType = ValueType.BOOLEAN;
  75 + } else if (typeof this.modelValue === 'number') {
  76 + if (this.modelValue.toString().indexOf('.') === -1) {
  77 + this.valueType = ValueType.INTEGER;
  78 + } else {
  79 + this.valueType = ValueType.DOUBLE;
  80 + }
  81 + } else {
  82 + this.valueType = ValueType.STRING;
  83 + }
  84 + }
  85 +
  86 + updateView() {
  87 + if (this.inputForm.valid || this.valueType === ValueType.BOOLEAN) {
  88 + this.propagateChange(this.modelValue);
  89 + } else {
  90 + this.propagateChange(null);
  91 + }
  92 + }
  93 +
  94 + onValueTypeChanged() {
  95 + if (this.valueType === ValueType.BOOLEAN) {
  96 + this.modelValue = false;
  97 + } else {
  98 + this.modelValue = null;
  99 + }
  100 + this.updateView();
  101 + }
  102 +
  103 + onValueChanged() {
  104 + this.updateView();
  105 + }
  106 +
  107 +}
... ...
... ... @@ -15,9 +15,47 @@
15 15 ///
16 16
17 17
  18 +import { AlarmSeverity } from '@shared/models/alarm.models';
  19 +
18 20 export enum DataKeyType {
19 21 timeseries = 'timeseries',
20 22 attribute = 'attribute',
21 23 function = 'function',
22 24 alarm = 'alarm'
23 25 }
  26 +
  27 +export enum LatestTelemetry {
  28 + LATEST_TELEMETRY = 'LATEST_TELEMETRY'
  29 +}
  30 +
  31 +export enum AttributeScope {
  32 + CLIENT_SCOPE = 'CLIENT_SCOPE',
  33 + SERVER_SCOPE = 'SERVER_SCOPE',
  34 + SHARED_SCOPE = 'SHARED_SCOPE'
  35 +}
  36 +
  37 +export type TelemetryType = LatestTelemetry | AttributeScope;
  38 +
  39 +export const telemetryTypeTranslations = new Map<TelemetryType, string>(
  40 + [
  41 + [LatestTelemetry.LATEST_TELEMETRY, 'attribute.scope-latest-telemetry'],
  42 + [AttributeScope.CLIENT_SCOPE, 'attribute.scope-client'],
  43 + [AttributeScope.SERVER_SCOPE, 'attribute.scope-server'],
  44 + [AttributeScope.SHARED_SCOPE, 'attribute.scope-shared']
  45 + ]
  46 +);
  47 +
  48 +export const isClientSideTelemetryType = new Map<TelemetryType, boolean>(
  49 + [
  50 + [LatestTelemetry.LATEST_TELEMETRY, true],
  51 + [AttributeScope.CLIENT_SCOPE, true],
  52 + [AttributeScope.SERVER_SCOPE, false],
  53 + [AttributeScope.SHARED_SCOPE, false]
  54 + ]
  55 +);
  56 +
  57 +export interface AttributeData {
  58 + lastUpdateTs: number;
  59 + key: string;
  60 + value: any;
  61 +}
... ...
... ... @@ -70,7 +70,7 @@ import {TimeintervalComponent} from '@shared/components/time/timeinterval.compon
70 70 import {DatetimePeriodComponent} from '@shared/components/time/datetime-period.component';
71 71 import {EnumToArrayPipe} from '@shared/pipe/enum-to-array.pipe';
72 72 import {ClipboardModule} from 'ngx-clipboard';
73   -// import { ValueInputComponent } from '@shared/components/value-input.component';
  73 +import { ValueInputComponent } from '@shared/components/value-input.component';
74 74 import {FullscreenDirective} from '@shared/components/fullscreen.directive';
75 75 import {HighlightPipe} from '@shared/pipe/highlight.pipe';
76 76 import {DashboardAutocompleteComponent} from '@shared/components/dashboard-autocomplete.component';
... ... @@ -93,7 +93,6 @@ import { JsonObjectEditComponent } from './components/json-object-edit.component
93 93 MillisecondsToTimeStringPipe,
94 94 EnumToArrayPipe,
95 95 HighlightPipe
96   -// IntervalCountPipe,
97 96 ],
98 97 entryComponents: [
99 98 TbSnackBarComponent,
... ... @@ -116,7 +115,7 @@ import { JsonObjectEditComponent } from './components/json-object-edit.component
116 115 TimeintervalComponent,
117 116 DatetimePeriodComponent,
118 117 DatetimeComponent,
119   -// ValueInputComponent,
  118 + ValueInputComponent,
120 119 DashboardAutocompleteComponent,
121 120 EntitySubTypeAutocompleteComponent,
122 121 EntitySubTypeSelectComponent,
... ... @@ -202,7 +201,7 @@ import { JsonObjectEditComponent } from './components/json-object-edit.component
202 201 RelationTypeAutocompleteComponent,
203 202 SocialSharePanelComponent,
204 203 JsonObjectEditComponent,
205   -// ValueInputComponent,
  204 + ValueInputComponent,
206 205 MatButtonModule,
207 206 MatCheckboxModule,
208 207 MatIconModule,
... ...
... ... @@ -296,7 +296,9 @@
296 296 "add-to-dashboard": "Add to dashboard",
297 297 "add-widget-to-dashboard": "Add widget to dashboard",
298 298 "selected-attributes": "{ count, plural, 1 {1 attribute} other {# attributes} } selected",
299   - "selected-telemetry": "{ count, plural, 1 {1 telemetry unit} other {# telemetry units} } selected"
  299 + "selected-telemetry": "{ count, plural, 1 {1 telemetry unit} other {# telemetry units} } selected",
  300 + "no-attributes-text": "No attributes found",
  301 + "no-telemetry-text": "No telemetry found"
300 302 },
301 303 "audit-log": {
302 304 "audit": "Audit",
... ... @@ -1491,11 +1493,14 @@
1491 1493 "type": "Value type",
1492 1494 "string": "String",
1493 1495 "string-value": "String value",
  1496 + "string-value-required": "String value is required",
1494 1497 "integer": "Integer",
1495 1498 "integer-value": "Integer value",
  1499 + "integer-value-required": "Integer value is required",
1496 1500 "invalid-integer-value": "Invalid integer value",
1497 1501 "double": "Double",
1498 1502 "double-value": "Double value",
  1503 + "double-value-required": "Double value is required",
1499 1504 "boolean": "Boolean",
1500 1505 "boolean-value": "Boolean value",
1501 1506 "false": "False",
... ...