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