20 changed files with 1390 additions and 8 deletions
@ -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); |
||||
|
} |
||||
|
} |
||||
@ -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> |
||||
@ -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); |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
} |
||||
@ -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> </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> |
||||
@ -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 |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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:
|
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -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> |
||||
@ -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; |
||||
|
} |
||||
|
|
||||
@ -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(); |
||||
|
} |
||||
|
} |
||||
@ -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(); |
||||
|
} |
||||
|
} |
||||
@ -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> |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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(); |
||||
|
} |
||||
|
|
||||
|
} |
||||
Loading…
Reference in new issue