Browse Source

Attributes table

pull/2147/head
Igor Kulikov 7 years ago
parent
commit
9a9373791d
  1. 74
      ui-ngx/src/app/core/http/attribute.service.ts
  2. 61
      ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.html
  3. 96
      ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.ts
  4. 192
      ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html
  5. 63
      ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.scss
  6. 341
      ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts
  7. 42
      ui-ngx/src/app/modules/home/components/attribute/edit-attribute-value-panel.component.html
  8. 19
      ui-ngx/src/app/modules/home/components/attribute/edit-attribute-value-panel.component.scss
  9. 88
      ui-ngx/src/app/modules/home/components/attribute/edit-attribute-value-panel.component.ts
  10. 3
      ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.scss
  11. 4
      ui-ngx/src/app/modules/home/components/entity/entity-tabs.component.ts
  12. 15
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  13. 135
      ui-ngx/src/app/modules/home/models/datasource/attribute-datasource.ts
  14. 15
      ui-ngx/src/app/modules/home/pages/device/device-tabs.component.html
  15. 63
      ui-ngx/src/app/shared/components/value-input.component.html
  16. 28
      ui-ngx/src/app/shared/components/value-input.component.scss
  17. 107
      ui-ngx/src/app/shared/components/value-input.component.ts
  18. 38
      ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts
  19. 7
      ui-ngx/src/app/shared/shared.module.ts
  20. 7
      ui-ngx/src/assets/locale/locale.constant-en_US.json

74
ui-ngx/src/app/core/http/attribute.service.ts

@ -0,0 +1,74 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Injectable } from '@angular/core';
import { defaultHttpOptions } from './http-utils';
import { forkJoin, Observable, of } from 'rxjs/index';
import { HttpClient } from '@angular/common/http';
import { EntityId } from '@shared/models/id/entity-id';
import { AttributeData, AttributeScope } from '@shared/models/telemetry/telemetry.models';
@Injectable({
providedIn: 'root'
})
export class AttributeService {
constructor(
private http: HttpClient
) { }
public getEntityAttributes(entityId: EntityId, attributeScope: AttributeScope,
ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Array<AttributeData>> {
return this.http.get<Array<AttributeData>>(`/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/values/attributes/` +
`${attributeScope}`,
defaultHttpOptions(ignoreLoading, ignoreErrors));
}
public deleteEntityAttributes(entityId: EntityId, attributeScope: AttributeScope, attributes: Array<AttributeData>,
ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<any> {
const keys = attributes.map(attribute => attribute.key).join(',');
return this.http.delete(`/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/${attributeScope}` +
`?keys=${keys}`,
defaultHttpOptions(ignoreLoading, ignoreErrors));
}
public saveEntityAttributes(entityId: EntityId, attributeScope: AttributeScope, attributes: Array<AttributeData>,
ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<any> {
const attributesData: {[key: string]: any} = {};
const deleteAttributes: AttributeData[] = [];
attributes.forEach((attribute) => {
if (attribute.value !== null) {
attributesData[attribute.key] = attribute.value;
} else {
deleteAttributes.push(attribute);
}
});
let deleteEntityAttributesObservable: Observable<any>;
if (deleteAttributes.length) {
deleteEntityAttributesObservable = this.deleteEntityAttributes(entityId, attributeScope, deleteAttributes);
} else {
deleteEntityAttributesObservable = of(null);
}
let saveEntityAttributesObservable: Observable<any>;
if (Object.keys(attributesData).length) {
saveEntityAttributesObservable = this.http.post(`/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/${attributeScope}`,
attributesData, defaultHttpOptions(ignoreLoading, ignoreErrors));
} else {
saveEntityAttributesObservable = of(null);
}
return forkJoin(saveEntityAttributesObservable, deleteEntityAttributesObservable);
}
}

61
ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.html

@ -0,0 +1,61 @@
<!--
Copyright © 2016-2019 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<form #attributeForm="ngForm" [formGroup]="attributeFormGroup" (ngSubmit)="add()" style="min-width: 400px;">
<mat-toolbar fxLayout="row" color="primary">
<h2>{{ 'attribute.add' | translate }}</h2>
<span fxFlex></span>
<button mat-button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div mat-dialog-content>
<fieldset [disabled]="isLoading$ | async">
<mat-form-field class="mat-block">
<mat-label translate>attribute.key</mat-label>
<input matInput formControlName="key" required>
<mat-error *ngIf="attributeFormGroup.get('key').hasError('required')">
{{ 'attribute.key-required' | translate }}
</mat-error>
</mat-form-field>
<tb-value-input
formControlName="value"
requiredText="attribute.value-required">
</tb-value-input>
</fieldset>
</div>
<div mat-dialog-actions fxLayout="row">
<span fxFlex></span>
<button mat-button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || attributeFormGroup.invalid || !attributeFormGroup.dirty">
{{ 'action.add' | translate }}
</button>
<button mat-button color="primary"
style="margin-right: 20px;"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.cancel' | translate }}
</button>
</div>
</form>

96
ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.ts

@ -0,0 +1,96 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, Inject, OnInit, SkipSelf, ViewChild } from '@angular/core';
import { ErrorStateMatcher, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms';
import {
CONTAINS_TYPE,
EntityRelation,
EntitySearchDirection,
RelationTypeGroup
} from '@shared/models/relation.models';
import { EntityRelationService } from '@core/http/entity-relation.service';
import { EntityId } from '@shared/models/id/entity-id';
import { forkJoin, Observable } from 'rxjs';
import { JsonObjectEditComponent } from '@app/shared/components/json-object-edit.component';
import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component';
import { AttributeData, AttributeScope } from '@shared/models/telemetry/telemetry.models';
import { AttributeService } from '@core/http/attribute.service';
export interface AddAttributeDialogData {
entityId: EntityId;
attributeScope: AttributeScope;
}
@Component({
selector: 'tb-add-attribute-dialog',
templateUrl: './add-attribute-dialog.component.html',
providers: [{provide: ErrorStateMatcher, useExisting: AddAttributeDialogComponent}],
styleUrls: []
})
export class AddAttributeDialogComponent extends DialogComponent<AddAttributeDialogComponent, boolean>
implements OnInit, ErrorStateMatcher {
attributeFormGroup: FormGroup;
submitted = false;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: AddAttributeDialogData,
private attributeService: AttributeService,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<AddAttributeDialogComponent, boolean>,
public fb: FormBuilder) {
super(store, router, dialogRef);
}
ngOnInit(): void {
this.attributeFormGroup = this.fb.group({
key: ['', [Validators.required]],
value: [null, [Validators.required]]
});
}
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
const customErrorState = !!(control && control.invalid && this.submitted);
return originalErrorState || customErrorState;
}
cancel(): void {
this.dialogRef.close(false);
}
add(): void {
this.submitted = true;
const attribute: AttributeData = {
lastUpdateTs: null,
key: this.attributeFormGroup.get('key').value,
value: this.attributeFormGroup.get('value').value
};
this.attributeService.saveEntityAttributes(this.data.entityId,
this.data.attributeScope, [attribute]).subscribe(
() => {
this.dialogRef.close(true);
}
);
}
}

192
ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html

@ -0,0 +1,192 @@
<!--
Copyright © 2016-2019 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="mat-padding tb-entity-table tb-absolute-fill">
<div fxFlex fxLayout="column" class="mat-elevation-z1 tb-entity-table-content">
<mat-toolbar class="mat-table-toolbar" [fxShow]="mode === 'default' && !textSearchMode && dataSource.selection.isEmpty()">
<div class="mat-toolbar-tools">
<span class="tb-entity-table-title">{{telemetryTypeTranslationsMap.get(attributeScope) | translate}}</span>
<mat-form-field class="mat-block tb-attribute-scope" style="width: 200px;" *ngIf="!disableAttributeScopeSelection">
<mat-label translate>attribute.attributes-scope</mat-label>
<mat-select [disabled]="(isLoading$ | async) || attributeScopeSelectionReadonly"
matInput [ngModel]="attributeScope"
(ngModelChange)="attributeScopeChanged($event)">
<mat-option *ngFor="let scope of attributeScopes" [value]="scope">
{{ telemetryTypeTranslationsMap.get(scope) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<span fxFlex></span>
<button *ngIf="!isClientSideTelemetryTypeMap.get(attributeScope)"
mat-button mat-icon-button
[disabled]="isLoading$ | async"
(click)="addAttribute($event)"
matTooltip="{{ 'action.add' | translate }}"
matTooltipPosition="above">
<mat-icon>add</mat-icon>
</button>
<button *ngIf="!isClientSideTelemetryTypeMap.get(attributeScope)"
mat-button mat-icon-button
[disabled]="isLoading$ | async"
(click)="reloadAttributes()"
matTooltip="{{ 'action.refresh' | translate }}"
matTooltipPosition="above">
<mat-icon>refresh</mat-icon>
</button>
<button mat-button mat-icon-button
[disabled]="isLoading$ | async"
(click)="enterFilterMode()"
matTooltip="{{ 'action.search' | translate }}"
matTooltipPosition="above">
<mat-icon>search</mat-icon>
</button>
</div>
</mat-toolbar>
<mat-toolbar class="mat-table-toolbar" [fxShow]="mode === 'default' && textSearchMode && dataSource.selection.isEmpty()">
<div class="mat-toolbar-tools">
<button mat-button mat-icon-button
matTooltip="{{ 'action.search' | translate }}"
matTooltipPosition="above">
<mat-icon>search</mat-icon>
</button>
<mat-form-field fxFlex>
<mat-label>&nbsp;</mat-label>
<input #searchInput matInput
[(ngModel)]="pageLink.textSearch"
placeholder="{{ 'common.enter-search' | translate }}"/>
</mat-form-field>
<button mat-button mat-icon-button (click)="exitFilterMode()"
matTooltip="{{ 'action.close' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button>
</div>
</mat-toolbar>
<mat-toolbar class="mat-table-toolbar" color="primary" [fxShow]="mode === 'default' && !dataSource.selection.isEmpty()">
<div class="mat-toolbar-tools">
<span>
{{ translate.get(
attributeScope === latestTelemetryTypes.LATEST_TELEMETRY
? 'attribute.selected-telemetry' : 'attribute.selected-attributes',
{count: dataSource.selection.selected.length}) | async }}
</span>
<span fxFlex></span>
<button mat-button mat-icon-button [disabled]="isLoading$ | async"
matTooltip="{{ 'action.delete' | translate }}"
matTooltipPosition="above"
(click)="deleteAttributes($event)">
<mat-icon>delete</mat-icon>
</button>
<button mat-button mat-raised-button
color="accent"
[disabled]="isLoading$ | async"
matTooltip="{{ 'attribute.show-on-widget' | translate }}"
matTooltipPosition="above"
(click)="enterWidgetMode()">
<mat-icon>now_widgets</mat-icon>
<span translate>attribute.show-on-widget</span>
</button>
</div>
</mat-toolbar>
<mat-toolbar class="mat-table-toolbar" color="primary" [fxShow]="mode === 'widget'">
<div class="mat-toolbar-tools">
<div fxFlex fxLayout="row" fxLayoutAlign="start">
<span class="tb-details-subtitle">{{ 'widgets-bundle.current' | translate }}</span>
<span fxFlex>TODO:</span>
</div>
<!--button mat-button mat-raised-button
color="accent"
[disabled]="isLoading$ | async"
matTooltip="{{ 'attribute.show-on-widget' | translate }}"
matTooltipPosition="above"
(click)="enterWidgetMode()">
<mat-icon>now_widgets</mat-icon>
<span translate>attribute.show-on-widget</span>
</button-->
<button mat-button mat-icon-button
[disabled]="isLoading$ | async"
matTooltip="{{ 'action.close' | translate }}"
matTooltipPosition="above"
(click)="exitWidgetMode()">
<mat-icon>close</mat-icon>
</button>
</div>
</mat-toolbar>
<div fxFlex class="table-container" [fxShow]="mode !== 'widget'">
<mat-table [dataSource]="dataSource"
matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="(pageLink.sortOrder.direction + '').toLowerCase()" matSortDisableClear>
<ng-container matColumnDef="select" sticky>
<mat-header-cell *matHeaderCellDef>
<mat-checkbox (change)="$event ? dataSource.masterToggle() : null"
[checked]="dataSource.selection.hasValue() && (dataSource.isAllSelected() | async)"
[indeterminate]="dataSource.selection.hasValue() && !(dataSource.isAllSelected() | async)">
</mat-checkbox>
</mat-header-cell>
<mat-cell *matCellDef="let attribute">
<mat-checkbox (click)="$event.stopPropagation()"
(change)="$event ? dataSource.selection.toggle(attribute) : null"
[checked]="dataSource.selection.isSelected(attribute)">
</mat-checkbox>
</mat-cell>
</ng-container>
<ng-container matColumnDef="lastUpdateTs">
<mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'attribute.last-update-time' | translate }} </mat-header-cell>
<mat-cell *matCellDef="let attribute">
{{ attribute.lastUpdateTs | date:'yyyy-MM-dd HH:mm:ss' }}
</mat-cell>
</ng-container>
<ng-container matColumnDef="key">
<mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'attribute.key' | translate }} </mat-header-cell>
<mat-cell *matCellDef="let attribute">
{{ attribute.key }}
</mat-cell>
</ng-container>
<ng-container matColumnDef="value">
<mat-header-cell *matHeaderCellDef> {{ 'attribute.value' | translate }} </mat-header-cell>
<mat-cell *matCellDef="let attribute"
class="tb-value-cell"
(click)="editAttribute($event, attribute)">
<span fxFlex>{{attribute.value}}</span>
<span [fxShow]="!isClientSideTelemetryTypeMap.get(attributeScope)">
<mat-icon>edit</mat-icon>
</span>
</mat-cell>
</ng-container>
<mat-header-row [ngClass]="{'mat-row-select': true}" *matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row>
<mat-row [ngClass]="{'mat-row-select': true,
'mat-selected': dataSource.selection.isSelected(attribute)}"
*matRowDef="let attribute; columns: displayedColumns;" (click)="dataSource.selection.toggle(attribute)"></mat-row>
</mat-table>
<span [fxShow]="dataSource.isEmpty() | async"
fxLayoutAlign="center center"
class="no-data-found" translate>{{
attributeScope === latestTelemetryTypes.LATEST_TELEMETRY
? 'attribute.no-telemetry-text'
: 'attribute.no-attributes-text'
}}</span>
</div>
<mat-divider [fxShow]="mode !== 'widget'"></mat-divider>
<mat-paginator [fxShow]="mode !== 'widget'"
[length]="dataSource.total() | async"
[pageIndex]="pageLink.page"
[pageSize]="pageLink.pageSize"
[pageSizeOptions]="[10, 20, 30]"></mat-paginator>
<div fxFlex [fxShow]="mode === 'widget'">
Coming soon!
</div>
</div>
</div>

63
ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.scss

@ -0,0 +1,63 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
width: 100%;
height: 100%;
.tb-entity-table {
.tb-entity-table-content {
width: 100%;
height: 100%;
background: #fff;
.tb-entity-table-title {
padding-right: 20px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.table-container {
overflow: auto;
}
}
}
}
:host ::ng-deep {
.mat-sort-header-sorted .mat-sort-header-arrow {
opacity: 1 !important;
}
mat-form-field.tb-attribute-scope {
font-size: 16px;
.mat-form-field-wrapper {
padding-bottom: 0;
}
.mat-form-field-underline {
bottom: 0;
}
}
mat-cell.tb-value-cell {
cursor: pointer;
mat-icon {
height: 16px;
width: 16px;
font-size: 16px;
color: #757575
}
}
}

341
ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts

@ -0,0 +1,341 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
AfterViewInit,
ChangeDetectionStrategy,
Component,
ElementRef,
Input,
OnInit,
ViewChild,
ViewContainerRef
} from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { PageLink } from '@shared/models/page/page-link';
import { MatPaginator } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { TranslateService } from '@ngx-translate/core';
import { MatDialog } from '@angular/material/dialog';
import { DialogService } from '@core/services/dialog.service';
import { Direction, SortOrder } from '@shared/models/page/sort-order';
import { fromEvent, merge } from 'rxjs';
import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators';
import { EntityId } from '@shared/models/id/entity-id';
import {
AttributeData,
AttributeScope,
isClientSideTelemetryType, LatestTelemetry,
TelemetryType,
telemetryTypeTranslations
} from '@shared/models/telemetry/telemetry.models';
import { AttributeDatasource } from '@home/models/datasource/attribute-datasource';
import { AttributeService } from '@app/core/http/attribute.service';
import { EntityType } from '@shared/models/entity-type.models';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { RelationDialogComponent, RelationDialogData } from '@home/components/relation/relation-dialog.component';
import {
AddAttributeDialogComponent,
AddAttributeDialogData
} from '@home/components/attribute/add-attribute-dialog.component';
import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
import { TIMEWINDOW_PANEL_DATA, TimewindowPanelComponent } from '@shared/components/time/timewindow-panel.component';
import {
EDIT_ATTRIBUTE_VALUE_PANEL_DATA,
EditAttributeValuePanelComponent,
EditAttributeValuePanelData
} from './edit-attribute-value-panel.component';
import { ComponentPortal, PortalInjector } from '@angular/cdk/portal';
@Component({
selector: 'tb-attribute-table',
templateUrl: './attribute-table.component.html',
styleUrls: ['./attribute-table.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AttributeTableComponent extends PageComponent implements AfterViewInit, OnInit {
telemetryTypeTranslationsMap = telemetryTypeTranslations;
isClientSideTelemetryTypeMap = isClientSideTelemetryType;
latestTelemetryTypes = LatestTelemetry;
mode: 'default' | 'widget' = 'default';
attributeScopes: Array<string> = [];
attributeScope: TelemetryType;
displayedColumns = ['select', 'lastUpdateTs', 'key', 'value'];
pageLink: PageLink;
textSearchMode = false;
dataSource: AttributeDatasource;
activeValue = false;
dirtyValue = false;
entityIdValue: EntityId;
attributeScopeSelectionReadonly = false;
viewsInited = false;
private disableAttributeScopeSelectionValue: boolean;
get disableAttributeScopeSelection(): boolean {
return this.disableAttributeScopeSelectionValue;
}
@Input()
set disableAttributeScopeSelection(value: boolean) {
this.disableAttributeScopeSelectionValue = coerceBooleanProperty(value);
}
@Input()
defaultAttributeScope: TelemetryType;
@Input()
set active(active: boolean) {
if (this.activeValue !== active) {
this.activeValue = active;
if (this.activeValue && this.dirtyValue) {
this.dirtyValue = false;
if (this.viewsInited) {
this.updateData(true);
}
}
}
}
@Input()
set entityId(entityId: EntityId) {
if (this.entityIdValue !== entityId) {
this.entityIdValue = entityId;
this.resetSortAndFilter(this.activeValue);
if (!this.activeValue) {
this.dirtyValue = true;
}
}
}
@ViewChild('searchInput', {static: false}) searchInputField: ElementRef;
@ViewChild(MatPaginator, {static: false}) paginator: MatPaginator;
@ViewChild(MatSort, {static: false}) sort: MatSort;
constructor(protected store: Store<AppState>,
private attributeService: AttributeService,
public translate: TranslateService,
public dialog: MatDialog,
private overlay: Overlay,
private viewContainerRef: ViewContainerRef,
private dialogService: DialogService) {
super(store);
this.dirtyValue = !this.activeValue;
const sortOrder: SortOrder = { property: 'key', direction: Direction.ASC };
this.pageLink = new PageLink(10, 0, null, sortOrder);
this.dataSource = new AttributeDatasource(this.attributeService, this.translate);
}
ngOnInit() {
}
attributeScopeChanged(attributeScope: TelemetryType) {
this.attributeScope = attributeScope;
this.mode = 'default';
this.updateData(true);
}
ngAfterViewInit() {
fromEvent(this.searchInputField.nativeElement, 'keyup')
.pipe(
debounceTime(150),
distinctUntilChanged(),
tap(() => {
this.paginator.pageIndex = 0;
this.updateData();
})
)
.subscribe();
this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0);
merge(this.sort.sortChange, this.paginator.page)
.pipe(
tap(() => this.updateData())
)
.subscribe();
this.viewsInited = true;
if (this.activeValue && this.entityIdValue) {
this.updateData(true);
}
}
updateData(reload: boolean = false) {
this.pageLink.page = this.paginator.pageIndex;
this.pageLink.pageSize = this.paginator.pageSize;
this.pageLink.sortOrder.property = this.sort.active;
this.pageLink.sortOrder.direction = Direction[this.sort.direction.toUpperCase()];
this.dataSource.loadAttributes(this.entityIdValue, this.attributeScope, this.pageLink, reload);
}
enterFilterMode() {
this.textSearchMode = true;
this.pageLink.textSearch = '';
setTimeout(() => {
this.searchInputField.nativeElement.focus();
this.searchInputField.nativeElement.setSelectionRange(0, 0);
}, 10);
}
exitFilterMode() {
this.textSearchMode = false;
this.pageLink.textSearch = null;
this.paginator.pageIndex = 0;
this.updateData();
}
resetSortAndFilter(update: boolean = true) {
const entityType = this.entityIdValue.entityType;
if (entityType === EntityType.DEVICE || entityType === EntityType.ENTITY_VIEW) {
this.attributeScopes = Object.keys(AttributeScope);
this.attributeScopeSelectionReadonly = false;
} else {
this.attributeScopes = [AttributeScope.SERVER_SCOPE];
this.attributeScopeSelectionReadonly = true;
}
this.mode = 'default';
this.attributeScope = this.defaultAttributeScope;
this.pageLink.textSearch = null;
if (this.viewsInited) {
this.paginator.pageIndex = 0;
const sortable = this.sort.sortables.get('key');
this.sort.active = sortable.id;
this.sort.direction = 'asc';
if (update) {
this.updateData(true);
}
}
}
reloadAttributes() {
this.updateData(true);
}
addAttribute($event: Event) {
if ($event) {
$event.stopPropagation();
}
this.dialog.open<AddAttributeDialogComponent, AddAttributeDialogData, boolean>(AddAttributeDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
entityId: this.entityIdValue,
attributeScope: this.attributeScope as AttributeScope
}
}).afterClosed().subscribe(
(res) => {
if (res) {
this.reloadAttributes();
}
}
);
}
editAttribute($event: Event, attribute: AttributeData) {
if ($event) {
$event.stopPropagation();
}
const target = $event.target || $event.srcElement || $event.currentTarget;
const config = new OverlayConfig();
config.backdropClass = 'cdk-overlay-transparent-backdrop';
config.hasBackdrop = true;
const connectedPosition: ConnectedPosition = {
originX: 'end',
originY: 'center',
overlayX: 'end',
overlayY: 'center'
};
config.positionStrategy = this.overlay.position().flexibleConnectedTo(target as HTMLElement)
.withPositions([connectedPosition]);
const overlayRef = this.overlay.create(config);
overlayRef.backdropClick().subscribe(() => {
overlayRef.dispose();
});
const injectionTokens = new WeakMap<any, any>([
[EDIT_ATTRIBUTE_VALUE_PANEL_DATA, {
attributeValue: attribute.value
} as EditAttributeValuePanelData],
[OverlayRef, overlayRef]
]);
const injector = new PortalInjector(this.viewContainerRef.injector, injectionTokens);
const componentRef = overlayRef.attach(new ComponentPortal(EditAttributeValuePanelComponent,
this.viewContainerRef, injector));
componentRef.onDestroy(() => {
if (componentRef.instance.result !== null) {
const attributeValue = componentRef.instance.result;
const updatedAttribute = {...attribute};
updatedAttribute.value = attributeValue;
this.attributeService.saveEntityAttributes(this.entityIdValue,
this.attributeScope as AttributeScope, [updatedAttribute]).subscribe(
() => {
this.reloadAttributes();
}
);
}
});
}
deleteAttributes($event: Event) {
if ($event) {
$event.stopPropagation();
}
if (this.dataSource.selection.selected.length > 0) {
this.dialogService.confirm(
this.translate.instant('attribute.delete-attributes-title', {count: this.dataSource.selection.selected.length}),
this.translate.instant('attribute.delete-attributes-text'),
this.translate.instant('action.no'),
this.translate.instant('action.yes'),
true
).subscribe((result) => {
if (result) {
this.attributeService.deleteEntityAttributes(this.entityIdValue,
this.attributeScope as AttributeScope, this.dataSource.selection.selected).subscribe(
() => {
this.reloadAttributes();
}
);
}
});
}
}
enterWidgetMode() {
this.mode = 'widget';
// TODO:
}
exitWidgetMode() {
this.mode = 'default';
this.reloadAttributes();
// TODO:
}
}

42
ui-ngx/src/app/modules/home/components/attribute/edit-attribute-value-panel.component.html

@ -0,0 +1,42 @@
<!--
Copyright © 2016-2019 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<form #attributeForm="ngForm"
class="mat-elevation-z1"
[formGroup]="attributeFormGroup" (ngSubmit)="update()" style="width: 400px; padding: 5px;">
<fieldset [disabled]="isLoading$ | async">
<tb-value-input
formControlName="value"
requiredText="attribute.value-required">
</tb-value-input>
</fieldset>
<div fxLayout="row" class="tb-panel-actions">
<span fxFlex></span>
<button mat-button color="primary"
style="margin-right: 20px;"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.cancel' | translate }}
</button>
<button mat-button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || attributeFormGroup.invalid || !attributeFormGroup.dirty">
{{ 'action.update' | translate }}
</button>
</div>
</form>

19
ui-ngx/src/app/modules/home/components/attribute/edit-attribute-value-panel.component.scss

@ -0,0 +1,19 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
background-color: #f9f9f9;
}

88
ui-ngx/src/app/modules/home/components/attribute/edit-attribute-value-panel.component.ts

@ -0,0 +1,88 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, Inject, InjectionToken, OnInit, SkipSelf, ViewChild } from '@angular/core';
import { ErrorStateMatcher, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms';
import {
CONTAINS_TYPE,
EntityRelation,
EntitySearchDirection,
RelationTypeGroup
} from '@shared/models/relation.models';
import { EntityRelationService } from '@core/http/entity-relation.service';
import { EntityId } from '@shared/models/id/entity-id';
import { forkJoin, Observable } from 'rxjs';
import { JsonObjectEditComponent } from '@app/shared/components/json-object-edit.component';
import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component';
import { AttributeData, AttributeScope } from '@shared/models/telemetry/telemetry.models';
import { AttributeService } from '@core/http/attribute.service';
import { PageComponent } from '@shared/components/page.component';
import { OverlayRef } from '@angular/cdk/overlay';
export const EDIT_ATTRIBUTE_VALUE_PANEL_DATA = new InjectionToken<any>('EditAttributeValuePanelData');
export interface EditAttributeValuePanelData {
attributeValue: any;
}
@Component({
selector: 'tb-edit-attribute-value-panel',
templateUrl: './edit-attribute-value-panel.component.html',
providers: [{provide: ErrorStateMatcher, useExisting: EditAttributeValuePanelComponent}],
styleUrls: ['./edit-attribute-value-panel.component.scss']
})
export class EditAttributeValuePanelComponent extends PageComponent implements OnInit, ErrorStateMatcher {
attributeFormGroup: FormGroup;
result: any = null;
submitted = false;
constructor(protected store: Store<AppState>,
@Inject(EDIT_ATTRIBUTE_VALUE_PANEL_DATA) public data: EditAttributeValuePanelData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public overlayRef: OverlayRef,
public fb: FormBuilder) {
super(store);
}
ngOnInit(): void {
this.attributeFormGroup = this.fb.group({
value: [this.data.attributeValue, [Validators.required]]
});
}
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
const customErrorState = !!(control && control.invalid && this.submitted);
return originalErrorState || customErrorState;
}
cancel(): void {
this.overlayRef.dispose();
}
update(): void {
this.submitted = true;
this.result = this.attributeFormGroup.get('value').value;
this.overlayRef.dispose();
}
}

3
ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.scss

@ -28,4 +28,7 @@
right: 0;
bottom: 0;
}
.mat-tab-label {
min-width: 40px;
}
}

4
ui-ngx/src/app/modules/home/components/entity/entity-tabs.component.ts

@ -38,10 +38,14 @@ import { AuthUser } from '@shared/models/user.model';
import { EntityType } from '@shared/models/entity-type.models';
import { AuditLogMode } from '@shared/models/audit-log.models';
import { DebugEventType, EventType } from '@shared/models/event.models';
import { AttributeScope, LatestTelemetry } from '@shared/models/telemetry/telemetry.models';
import { NULL_UUID } from '@shared/models/id/has-uuid';
export abstract class EntityTabsComponent<T extends BaseData<HasId>> extends PageComponent implements OnInit, AfterViewInit {
attributeScopes = AttributeScope;
latestTelemetryTypes = LatestTelemetry;
authorities = Authority;
entityTypes = EntityType;

15
ui-ngx/src/app/modules/home/components/home-components.module.ts

@ -31,6 +31,9 @@ import { RelationDialogComponent } from './relation/relation-dialog.component';
import { AlarmTableHeaderComponent } from '@home/components/alarm/alarm-table-header.component';
import { AlarmTableComponent } from '@home/components/alarm/alarm-table.component';
import { AlarmDetailsDialogComponent } from '@home/components/alarm/alarm-details-dialog.component';
import { AttributeTableComponent } from '@home/components/attribute/attribute-table.component';
import { AddAttributeDialogComponent } from './attribute/add-attribute-dialog.component';
import { EditAttributeValuePanelComponent } from './attribute/edit-attribute-value-panel.component';
@NgModule({
entryComponents: [
@ -39,7 +42,9 @@ import { AlarmDetailsDialogComponent } from '@home/components/alarm/alarm-detail
EventTableHeaderComponent,
RelationDialogComponent,
AlarmTableHeaderComponent,
AlarmDetailsDialogComponent
AlarmDetailsDialogComponent,
AddAttributeDialogComponent,
EditAttributeValuePanelComponent
],
declarations:
[
@ -56,7 +61,10 @@ import { AlarmDetailsDialogComponent } from '@home/components/alarm/alarm-detail
RelationDialogComponent,
AlarmTableHeaderComponent,
AlarmTableComponent,
AlarmDetailsDialogComponent
AlarmDetailsDialogComponent,
AttributeTableComponent,
AddAttributeDialogComponent,
EditAttributeValuePanelComponent
],
imports: [
CommonModule,
@ -72,7 +80,8 @@ import { AlarmDetailsDialogComponent } from '@home/components/alarm/alarm-detail
EventTableComponent,
RelationTableComponent,
AlarmTableComponent,
AlarmDetailsDialogComponent
AlarmDetailsDialogComponent,
AttributeTableComponent
]
})
export class HomeComponentsModule { }

135
ui-ngx/src/app/modules/home/models/datasource/attribute-datasource.ts

@ -0,0 +1,135 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { CollectionViewer, DataSource } from '@angular/cdk/typings/collections';
import { BehaviorSubject, Observable, of, ReplaySubject } from 'rxjs';
import { emptyPageData, PageData } from '@shared/models/page/page-data';
import { SelectionModel } from '@angular/cdk/collections';
import { PageLink } from '@shared/models/page/page-link';
import { catchError, map, publishReplay, refCount, take, tap } from 'rxjs/operators';
import { EntityId } from '@app/shared/models/id/entity-id';
import { TranslateService } from '@ngx-translate/core';
import {
AttributeData,
AttributeScope,
isClientSideTelemetryType,
TelemetryType
} from '@shared/models/telemetry/telemetry.models';
import { AttributeService } from '@core/http/attribute.service';
export class AttributeDatasource implements DataSource<AttributeData> {
private attributesSubject = new BehaviorSubject<AttributeData[]>([]);
private pageDataSubject = new BehaviorSubject<PageData<AttributeData>>(emptyPageData<AttributeData>());
public pageData$ = this.pageDataSubject.asObservable();
public selection = new SelectionModel<AttributeData>(true, []);
private allAttributes: Observable<Array<AttributeData>>;
constructor(private attributeService: AttributeService,
private translate: TranslateService) {}
connect(collectionViewer: CollectionViewer): Observable<AttributeData[] | ReadonlyArray<AttributeData>> {
return this.attributesSubject.asObservable();
}
disconnect(collectionViewer: CollectionViewer): void {
this.attributesSubject.complete();
this.pageDataSubject.complete();
}
loadAttributes(entityId: EntityId, attributesScope: TelemetryType,
pageLink: PageLink, reload: boolean = false): Observable<PageData<AttributeData>> {
if (reload) {
this.allAttributes = null;
}
const result = new ReplaySubject<PageData<AttributeData>>();
this.fetchAttributes(entityId, attributesScope, pageLink).pipe(
tap(() => {
this.selection.clear();
}),
catchError(() => of(emptyPageData<AttributeData>())),
).subscribe(
(pageData) => {
this.attributesSubject.next(pageData.data);
this.pageDataSubject.next(pageData);
result.next(pageData);
}
);
return result;
}
fetchAttributes(entityId: EntityId, attributesScope: TelemetryType,
pageLink: PageLink): Observable<PageData<AttributeData>> {
return this.getAllAttributes(entityId, attributesScope).pipe(
map((data) => pageLink.filterData(data))
);
}
getAllAttributes(entityId: EntityId, attributesScope: TelemetryType): Observable<Array<AttributeData>> {
if (!this.allAttributes) {
let attributesObservable: Observable<Array<AttributeData>>;
if (isClientSideTelemetryType.get(attributesScope)) {
attributesObservable = of([]);
// TODO:
} else {
attributesObservable = this.attributeService.getEntityAttributes(entityId, attributesScope as AttributeScope);
}
this.allAttributes = attributesObservable.pipe(
publishReplay(1),
refCount()
);
}
return this.allAttributes;
}
isAllSelected(): Observable<boolean> {
const numSelected = this.selection.selected.length;
return this.attributesSubject.pipe(
map((attributes) => numSelected === attributes.length)
);
}
isEmpty(): Observable<boolean> {
return this.attributesSubject.pipe(
map((attributes) => !attributes.length)
);
}
total(): Observable<number> {
return this.pageDataSubject.pipe(
map((pageData) => pageData.totalElements)
);
}
masterToggle() {
this.attributesSubject.pipe(
tap((attributes) => {
const numSelected = this.selection.selected.length;
if (numSelected === attributes.length) {
this.selection.clear();
} else {
attributes.forEach(row => {
this.selection.select(row);
});
}
}),
take(1)
).subscribe();
}
}

15
ui-ngx/src/app/modules/home/pages/device/device-tabs.component.html

@ -15,6 +15,21 @@
limitations under the License.
-->
<mat-tab *ngIf="entity"
label="{{ 'attribute.attributes' | translate }}" #attributesTab="matTab">
<tb-attribute-table [active]="attributesTab.isActive"
[entityId]="entity.id"
[defaultAttributeScope]="attributeScopes.CLIENT_SCOPE">
</tb-attribute-table>
</mat-tab>
<mat-tab *ngIf="entity"
label="{{ 'attribute.latest-telemetry' | translate }}" #telemetryTab="matTab">
<tb-attribute-table [active]="telemetryTab.isActive"
[entityId]="entity.id"
[defaultAttributeScope]="latestTelemetryTypes.LATEST_TELEMETRY"
disableAttributeScopeSelection>
</tb-attribute-table>
</mat-tab>
<mat-tab *ngIf="entity"
label="{{ 'alarm.alarms' | translate }}" #alarmsTab="matTab">
<tb-alarm-table [active]="alarmsTab.isActive" [entityId]="entity.id"></tb-alarm-table>

63
ui-ngx/src/app/shared/components/value-input.component.html

@ -0,0 +1,63 @@
<!--
Copyright © 2016-2019 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<form #inputForm="ngForm">
<section fxLayout="row" fxLayoutGap="8px">
<mat-form-field fxFlex="40" class="mat-block tb-value-type">
<mat-label translate>value.type</mat-label>
<mat-select [disabled]="disabled" matInput name="valueType" [(ngModel)]="valueType" (ngModelChange)="onValueTypeChanged()">
<mat-select-trigger>
<mat-icon svgIcon="{{ valueTypes.get(valueType).icon }}"></mat-icon>
<span>{{ valueTypes.get(valueType).name | translate }}</span>
</mat-select-trigger>
<mat-option *ngFor="let valueType of valueTypeKeys" [value]="valueType">
<mat-icon svgIcon="{{ valueTypes.get(valueType).icon }}"></mat-icon>
<span>{{ valueTypes.get(valueType).name | translate }}</span>
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field *ngIf="valueType === valueTypeEnum.STRING" fxFlex="60" class="mat-block">
<mat-label translate>value.string-value</mat-label>
<input [disabled]="disabled" matInput required name="value" #value="ngModel" [(ngModel)]="modelValue" (ngModelChange)="onValueChanged()"/>
<mat-error *ngIf="value.hasError('required')">
{{ (requiredText ? requiredText : 'value.string-value-required') | translate }}
</mat-error>
</mat-form-field>
<mat-form-field *ngIf="valueType === valueTypeEnum.INTEGER" fxFlex="60" class="mat-block">
<mat-label translate>value.integer-value</mat-label>
<input [disabled]="disabled" matInput required name="value" type="number" step="1" pattern="^-?[0-9]+$" #value="ngModel" [(ngModel)]="modelValue" (ngModelChange)="onValueChanged()"/>
<mat-error *ngIf="value.hasError('required')">
{{ (requiredText ? requiredText : 'value.integer-value-required') | translate }}
</mat-error>
<mat-error *ngIf="value.hasError('pattern')">
{{ 'value.invalid-integer-value' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field *ngIf="valueType === valueTypeEnum.DOUBLE" fxFlex="60" class="mat-block">
<mat-label translate>value.double-value</mat-label>
<input [disabled]="disabled" matInput required name="value" type="number" step="any" #value="ngModel" [(ngModel)]="modelValue" (ngModelChange)="onValueChanged()"/>
<mat-error *ngIf="value.hasError('required')">
{{ (requiredText ? requiredText : 'value.double-value-required') | translate }}
</mat-error>
</mat-form-field>
<div fxLayout="column" fxLayoutAlign="center" fxFlex="60" *ngIf="valueType === valueTypeEnum.BOOLEAN">
<mat-checkbox [disabled]="disabled" name="value" #value="ngModel" [(ngModel)]="modelValue" (ngModelChange)="onValueChanged()" style="margin-bottom: 0px;">
{{ (modelValue ? 'value.true' : 'value.false') | translate }}
</mat-checkbox>
</div>
</section>
</form>

28
ui-ngx/src/app/shared/components/value-input.component.scss

@ -0,0 +1,28 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host ::ng-deep {
mat-form-field.tb-value-type {
.mat-form-field-infix {
padding-bottom: 1px;
}
mat-select-trigger {
mat-icon {
margin-right: 16px;
}
}
}
}

107
ui-ngx/src/app/shared/components/value-input.component.ts

@ -0,0 +1,107 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, forwardRef, Input, OnInit, ViewChild } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR, NgForm } from '@angular/forms';
import { ValueType, valueTypesMap } from '@shared/models/constants';
@Component({
selector: 'tb-value-input',
templateUrl: './value-input.component.html',
styleUrls: ['./value-input.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ValueInputComponent),
multi: true
}
]
})
export class ValueInputComponent implements OnInit, ControlValueAccessor {
@Input() disabled: boolean;
@Input() requiredText: string;
@ViewChild('inputForm', {static: true}) inputForm: NgForm;
modelValue: any;
valueType: ValueType;
public valueTypeEnum = ValueType;
valueTypeKeys = Object.keys(ValueType);
valueTypes = valueTypesMap;
private propagateChange = null;
constructor() {
}
ngOnInit(): void {
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}
writeValue(value: any): void {
this.modelValue = value;
if (this.modelValue === true || this.modelValue === false) {
this.valueType = ValueType.BOOLEAN;
} else if (typeof this.modelValue === 'number') {
if (this.modelValue.toString().indexOf('.') === -1) {
this.valueType = ValueType.INTEGER;
} else {
this.valueType = ValueType.DOUBLE;
}
} else {
this.valueType = ValueType.STRING;
}
}
updateView() {
if (this.inputForm.valid || this.valueType === ValueType.BOOLEAN) {
this.propagateChange(this.modelValue);
} else {
this.propagateChange(null);
}
}
onValueTypeChanged() {
if (this.valueType === ValueType.BOOLEAN) {
this.modelValue = false;
} else {
this.modelValue = null;
}
this.updateView();
}
onValueChanged() {
this.updateView();
}
}

38
ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts

@ -15,9 +15,47 @@
///
import { AlarmSeverity } from '@shared/models/alarm.models';
export enum DataKeyType {
timeseries = 'timeseries',
attribute = 'attribute',
function = 'function',
alarm = 'alarm'
}
export enum LatestTelemetry {
LATEST_TELEMETRY = 'LATEST_TELEMETRY'
}
export enum AttributeScope {
CLIENT_SCOPE = 'CLIENT_SCOPE',
SERVER_SCOPE = 'SERVER_SCOPE',
SHARED_SCOPE = 'SHARED_SCOPE'
}
export type TelemetryType = LatestTelemetry | AttributeScope;
export const telemetryTypeTranslations = new Map<TelemetryType, string>(
[
[LatestTelemetry.LATEST_TELEMETRY, 'attribute.scope-latest-telemetry'],
[AttributeScope.CLIENT_SCOPE, 'attribute.scope-client'],
[AttributeScope.SERVER_SCOPE, 'attribute.scope-server'],
[AttributeScope.SHARED_SCOPE, 'attribute.scope-shared']
]
);
export const isClientSideTelemetryType = new Map<TelemetryType, boolean>(
[
[LatestTelemetry.LATEST_TELEMETRY, true],
[AttributeScope.CLIENT_SCOPE, true],
[AttributeScope.SERVER_SCOPE, false],
[AttributeScope.SHARED_SCOPE, false]
]
);
export interface AttributeData {
lastUpdateTs: number;
key: string;
value: any;
}

7
ui-ngx/src/app/shared/shared.module.ts

@ -70,7 +70,7 @@ import {TimeintervalComponent} from '@shared/components/time/timeinterval.compon
import {DatetimePeriodComponent} from '@shared/components/time/datetime-period.component';
import {EnumToArrayPipe} from '@shared/pipe/enum-to-array.pipe';
import {ClipboardModule} from 'ngx-clipboard';
// import { ValueInputComponent } from '@shared/components/value-input.component';
import { ValueInputComponent } from '@shared/components/value-input.component';
import {FullscreenDirective} from '@shared/components/fullscreen.directive';
import {HighlightPipe} from '@shared/pipe/highlight.pipe';
import {DashboardAutocompleteComponent} from '@shared/components/dashboard-autocomplete.component';
@ -93,7 +93,6 @@ import { JsonObjectEditComponent } from './components/json-object-edit.component
MillisecondsToTimeStringPipe,
EnumToArrayPipe,
HighlightPipe
// IntervalCountPipe,
],
entryComponents: [
TbSnackBarComponent,
@ -116,7 +115,7 @@ import { JsonObjectEditComponent } from './components/json-object-edit.component
TimeintervalComponent,
DatetimePeriodComponent,
DatetimeComponent,
// ValueInputComponent,
ValueInputComponent,
DashboardAutocompleteComponent,
EntitySubTypeAutocompleteComponent,
EntitySubTypeSelectComponent,
@ -202,7 +201,7 @@ import { JsonObjectEditComponent } from './components/json-object-edit.component
RelationTypeAutocompleteComponent,
SocialSharePanelComponent,
JsonObjectEditComponent,
// ValueInputComponent,
ValueInputComponent,
MatButtonModule,
MatCheckboxModule,
MatIconModule,

7
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -296,7 +296,9 @@
"add-to-dashboard": "Add to dashboard",
"add-widget-to-dashboard": "Add widget to dashboard",
"selected-attributes": "{ count, plural, 1 {1 attribute} other {# attributes} } selected",
"selected-telemetry": "{ count, plural, 1 {1 telemetry unit} other {# telemetry units} } selected"
"selected-telemetry": "{ count, plural, 1 {1 telemetry unit} other {# telemetry units} } selected",
"no-attributes-text": "No attributes found",
"no-telemetry-text": "No telemetry found"
},
"audit-log": {
"audit": "Audit",
@ -1491,11 +1493,14 @@
"type": "Value type",
"string": "String",
"string-value": "String value",
"string-value-required": "String value is required",
"integer": "Integer",
"integer-value": "Integer value",
"integer-value-required": "Integer value is required",
"invalid-integer-value": "Invalid integer value",
"double": "Double",
"double-value": "Double value",
"double-value-required": "Double value is required",
"boolean": "Boolean",
"boolean-value": "Boolean value",
"false": "False",

Loading…
Cancel
Save