Commit c3ebc2f20d4dc1e03418c80d278a2cbd34460918
1 parent
c317be2a
Widget Configuration: Widget Actions.
Showing
67 changed files
with
4197 additions
and
189 deletions
... | ... | @@ -5432,9 +5432,9 @@ |
5432 | 5432 | "dev": true |
5433 | 5433 | }, |
5434 | 5434 | "https-proxy-agent": { |
5435 | - "version": "2.2.2", | |
5436 | - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.2.tgz", | |
5437 | - "integrity": "sha512-c8Ndjc9Bkpfx/vCJueCPy0jlP4ccCCSNDp8xwCZzPjKJUm+B+u9WX2x98Qx4n1PiMNTWo3D7KK5ifNV/yJyRzg==", | |
5435 | + "version": "2.2.3", | |
5436 | + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.3.tgz", | |
5437 | + "integrity": "sha512-Ytgnz23gm2DVftnzqRRz2dOXZbGd2uiajSw/95bPp6v53zPRspQjLm/AfBgqbJ2qfeRXWIOMVLpp86+/5yX39Q==", | |
5438 | 5438 | "dev": true, |
5439 | 5439 | "requires": { |
5440 | 5440 | "agent-base": "^4.3.0", | ... | ... |
... | ... | @@ -15,7 +15,8 @@ |
15 | 15 | /// |
16 | 16 | |
17 | 17 | import { |
18 | - IWidgetSubscription, SubscriptionEntityInfo, | |
18 | + IWidgetSubscription, | |
19 | + SubscriptionEntityInfo, | |
19 | 20 | WidgetSubscriptionCallbacks, |
20 | 21 | WidgetSubscriptionContext, |
21 | 22 | WidgetSubscriptionOptions |
... | ... | @@ -48,6 +49,7 @@ import { deepClone, isDefined } from '@core/utils'; |
48 | 49 | import { AlarmSourceListener } from '@core/http/alarm.service'; |
49 | 50 | import { DatasourceListener } from '@core/api/datasource.service'; |
50 | 51 | import * as deepEqual from 'deep-equal'; |
52 | +import { EntityId } from '@app/shared/models/id/entity-id'; | |
51 | 53 | |
52 | 54 | export class WidgetSubscription implements IWidgetSubscription { |
53 | 55 | |
... | ... | @@ -339,7 +341,44 @@ export class WidgetSubscription implements IWidgetSubscription { |
339 | 341 | } |
340 | 342 | |
341 | 343 | getFirstEntityInfo(): SubscriptionEntityInfo { |
342 | - return undefined; | |
344 | + let entityId: EntityId; | |
345 | + let entityName: string; | |
346 | + if (this.type === widgetType.rpc) { | |
347 | + if (this.targetDeviceId) { | |
348 | + entityId = { | |
349 | + entityType: EntityType.DEVICE, | |
350 | + id: this.targetDeviceId | |
351 | + }; | |
352 | + entityName = this.targetDeviceName; | |
353 | + } | |
354 | + } else if (this.type === widgetType.alarm) { | |
355 | + if (this.alarmSource && this.alarmSource.entityType && this.alarmSource.entityId) { | |
356 | + entityId = { | |
357 | + entityType: this.alarmSource.entityType, | |
358 | + id: this.alarmSource.entityId | |
359 | + }; | |
360 | + entityName = this.alarmSource.entityName; | |
361 | + } | |
362 | + } else { | |
363 | + for (const datasource of this.datasources) { | |
364 | + if (datasource && datasource.entityType && datasource.entityId) { | |
365 | + entityId = { | |
366 | + entityType: datasource.entityType, | |
367 | + id: datasource.entityId | |
368 | + }; | |
369 | + entityName = datasource.entityName; | |
370 | + break; | |
371 | + } | |
372 | + } | |
373 | + } | |
374 | + if (entityId) { | |
375 | + return { | |
376 | + entityId, | |
377 | + entityName | |
378 | + }; | |
379 | + } else { | |
380 | + return null; | |
381 | + } | |
343 | 382 | } |
344 | 383 | |
345 | 384 | onAliasesChanged(aliasIds: Array<string>): boolean { | ... | ... |
... | ... | @@ -26,6 +26,10 @@ import { |
26 | 26 | ColorPickerDialogComponent, |
27 | 27 | ColorPickerDialogData |
28 | 28 | } from '@shared/components/dialog/color-picker-dialog.component'; |
29 | +import { | |
30 | + MaterialIconsDialogComponent, | |
31 | + MaterialIconsDialogData | |
32 | +} from '@shared/components/dialog/material-icons-dialog.component'; | |
29 | 33 | |
30 | 34 | @Injectable( |
31 | 35 | { |
... | ... | @@ -85,6 +89,17 @@ export class DialogService { |
85 | 89 | }).afterClosed(); |
86 | 90 | } |
87 | 91 | |
92 | + materialIconPicker(icon: string): Observable<string> { | |
93 | + return this.dialog.open<MaterialIconsDialogComponent, MaterialIconsDialogData, string>(MaterialIconsDialogComponent, | |
94 | + { | |
95 | + disableClose: true, | |
96 | + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], | |
97 | + data: { | |
98 | + icon | |
99 | + } | |
100 | + }).afterClosed(); | |
101 | + } | |
102 | + | |
88 | 103 | private permissionDenied() { |
89 | 104 | this.alert( |
90 | 105 | this.translate.instant('access.permission-denied'), | ... | ... |
... | ... | @@ -14,7 +14,7 @@ |
14 | 14 | /// limitations under the License. |
15 | 15 | /// |
16 | 16 | |
17 | -import { Inject, Injectable } from '@angular/core'; | |
17 | +import { Inject, Injectable, NgZone } from '@angular/core'; | |
18 | 18 | import { WINDOW } from '@core/services/window.service'; |
19 | 19 | import { ExceptionData } from '@app/shared/models/error.models'; |
20 | 20 | import { deepClone, deleteNullProperties, isDefined, isUndefined } from '@core/utils'; |
... | ... | @@ -28,6 +28,8 @@ import { alarmFields } from '@shared/models/alarm.models'; |
28 | 28 | import { materialColors } from '@app/shared/models/material.models'; |
29 | 29 | import { WidgetInfo } from '@home/models/widget-component.models'; |
30 | 30 | import jsonSchemaDefaults from 'json-schema-defaults'; |
31 | +import * as materialIconsCodepoints from '!raw-loader!material-design-icons/iconfont/codepoints'; | |
32 | +import { Observable, of, ReplaySubject } from 'rxjs'; | |
31 | 33 | |
32 | 34 | const varsRegex = /\$\{([^}]*)\}/g; |
33 | 35 | |
... | ... | @@ -58,6 +60,13 @@ const defaultAlarmFields: Array<string> = [ |
58 | 60 | alarmFields.status.keyName |
59 | 61 | ]; |
60 | 62 | |
63 | +const commonMaterialIcons: Array<string> = [ 'more_horiz', 'more_vert', 'open_in_new', | |
64 | + 'visibility', 'play_arrow', 'arrow_back', 'arrow_downward', | |
65 | + 'arrow_forward', 'arrow_upwards', 'close', 'refresh', 'menu', 'show_chart', 'multiline_chart', 'pie_chart', 'insert_chart', 'people', | |
66 | + 'person', 'domain', 'devices_other', 'now_widgets', 'dashboards', 'map', 'pin_drop', 'my_location', 'extension', 'search', | |
67 | + 'settings', 'notifications', 'notifications_active', 'info', 'info_outline', 'warning', 'list', 'file_download', 'import_export', | |
68 | + 'share', 'add', 'edit', 'done' ]; | |
69 | + | |
61 | 70 | @Injectable({ |
62 | 71 | providedIn: 'root' |
63 | 72 | }) |
... | ... | @@ -85,7 +94,10 @@ export class UtilsService { |
85 | 94 | |
86 | 95 | defaultAlarmDataKeys: Array<DataKey> = []; |
87 | 96 | |
97 | + materialIcons: Array<string> = []; | |
98 | + | |
88 | 99 | constructor(@Inject(WINDOW) private window: Window, |
100 | + private zone: NgZone, | |
89 | 101 | private translate: TranslateService) { |
90 | 102 | let frame: Element = null; |
91 | 103 | try { |
... | ... | @@ -282,6 +294,31 @@ export class UtilsService { |
282 | 294 | return datasources; |
283 | 295 | } |
284 | 296 | |
297 | + public getMaterialIcons(): Observable<Array<string>> { | |
298 | + if (this.materialIcons.length) { | |
299 | + return of(this.materialIcons); | |
300 | + } else { | |
301 | + const materialIconsSubject = new ReplaySubject<Array<string>>(); | |
302 | + this.zone.runOutsideAngular(() => { | |
303 | + const codepointsArray = materialIconsCodepoints | |
304 | + .split('\n') | |
305 | + .filter((codepoint) => codepoint && codepoint.length); | |
306 | + codepointsArray.forEach((codepoint) => { | |
307 | + const values = codepoint.split(' '); | |
308 | + if (values && values.length === 2) { | |
309 | + this.materialIcons.push(values[0]); | |
310 | + } | |
311 | + }); | |
312 | + materialIconsSubject.next(this.materialIcons); | |
313 | + }); | |
314 | + return materialIconsSubject.asObservable(); | |
315 | + } | |
316 | + } | |
317 | + | |
318 | + public getCommonMaterialIcons(): Array<string> { | |
319 | + return commonMaterialIcons; | |
320 | + } | |
321 | + | |
285 | 322 | public getMaterialColor(index: number) { |
286 | 323 | const colorIndex = index % materialColors.length; |
287 | 324 | return materialColors[colorIndex].value; | ... | ... |
... | ... | @@ -88,7 +88,7 @@ |
88 | 88 | <tb-timewindow *ngIf="widget.hasTimewindow" |
89 | 89 | #timewindowComponent |
90 | 90 | aggregation="{{widget.hasAggregation}}" |
91 | - [ngModel]="widget.widget.config.timewindow" | |
91 | + [ngModel]="widgetComponent.widget.config.timewindow" | |
92 | 92 | (ngModelChange)="widgetComponent.onTimewindowChanged($event)"> |
93 | 93 | </tb-timewindow> |
94 | 94 | </div> | ... | ... |
... | ... | @@ -15,12 +15,12 @@ |
15 | 15 | /// |
16 | 16 | |
17 | 17 | import { |
18 | - AfterViewInit, | |
18 | + AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, | |
19 | 19 | Component, |
20 | 20 | DoCheck, |
21 | 21 | Input, |
22 | 22 | IterableDiffers, |
23 | - KeyValueDiffers, | |
23 | + KeyValueDiffers, NgZone, | |
24 | 24 | OnChanges, |
25 | 25 | OnInit, |
26 | 26 | SimpleChanges, |
... | ... | @@ -162,7 +162,8 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo |
162 | 162 | private dialogService: DialogService, |
163 | 163 | private breakpointObserver: BreakpointObserver, |
164 | 164 | private differs: IterableDiffers, |
165 | - private kvDiffers: KeyValueDiffers) { | |
165 | + private kvDiffers: KeyValueDiffers, | |
166 | + private ngZone: NgZone) { | |
166 | 167 | super(store); |
167 | 168 | this.authUser = getCurrentAuthUser(store); |
168 | 169 | } |
... | ... | @@ -259,20 +260,24 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo |
259 | 260 | } |
260 | 261 | |
261 | 262 | onUpdateTimewindow(startTimeMs: number, endTimeMs: number, interval?: number): void { |
262 | - if (!this.originalDashboardTimewindow) { | |
263 | - this.originalDashboardTimewindow = deepClone(this.dashboardTimewindow); | |
264 | - } | |
265 | - this.dashboardTimewindow = toHistoryTimewindow(this.dashboardTimewindow, | |
266 | - startTimeMs, endTimeMs, interval, this.timeService); | |
267 | - this.dashboardTimewindowChangedSubject.next(this.dashboardTimewindow); | |
263 | + this.ngZone.run(() => { | |
264 | + if (!this.originalDashboardTimewindow) { | |
265 | + this.originalDashboardTimewindow = deepClone(this.dashboardTimewindow); | |
266 | + } | |
267 | + this.dashboardTimewindow = toHistoryTimewindow(this.dashboardTimewindow, | |
268 | + startTimeMs, endTimeMs, interval, this.timeService); | |
269 | + this.dashboardTimewindowChangedSubject.next(this.dashboardTimewindow); | |
270 | + }); | |
268 | 271 | } |
269 | 272 | |
270 | 273 | onResetTimewindow(): void { |
271 | - if (this.originalDashboardTimewindow) { | |
272 | - this.dashboardTimewindow = deepClone(this.originalDashboardTimewindow); | |
273 | - this.originalDashboardTimewindow = null; | |
274 | - this.dashboardTimewindowChangedSubject.next(this.dashboardTimewindow); | |
275 | - } | |
274 | + this.ngZone.run(() => { | |
275 | + if (this.originalDashboardTimewindow) { | |
276 | + this.dashboardTimewindow = deepClone(this.originalDashboardTimewindow); | |
277 | + this.originalDashboardTimewindow = null; | |
278 | + this.dashboardTimewindowChangedSubject.next(this.dashboardTimewindow); | |
279 | + } | |
280 | + }); | |
276 | 281 | } |
277 | 282 | |
278 | 283 | isAutofillHeight(): boolean { |
... | ... | @@ -456,7 +461,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo |
456 | 461 | this.gridsterOpts.draggable.enabled = this.isEdit; |
457 | 462 | } |
458 | 463 | |
459 | - private notifyGridsterOptionsChanged() { | |
464 | + public notifyGridsterOptionsChanged() { | |
460 | 465 | if (this.gridster && this.gridster.options) { |
461 | 466 | this.gridster.optionsChanged(); |
462 | 467 | } | ... | ... |
... | ... | @@ -14,7 +14,7 @@ |
14 | 14 | /// limitations under the License. |
15 | 15 | /// |
16 | 16 | |
17 | -import { Component, EventEmitter, Input, Output } from '@angular/core'; | |
17 | +import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; | |
18 | 18 | import { PageComponent } from '@shared/components/page.component'; |
19 | 19 | import { Store } from '@ngrx/store'; |
20 | 20 | import { AppState } from '@core/core.state'; | ... | ... |
... | ... | @@ -50,6 +50,12 @@ import { EntityAliasSelectComponent } from './alias/entity-alias-select.componen |
50 | 50 | import { DataKeysComponent } from '@home/components/widget/data-keys.component'; |
51 | 51 | import { DataKeyConfigDialogComponent } from './widget/data-key-config-dialog.component'; |
52 | 52 | import { DataKeyConfigComponent } from './widget/data-key-config.component'; |
53 | +import { LegendConfigPanelComponent } from './widget/legend-config-panel.component'; | |
54 | +import { LegendConfigComponent } from './widget/legend-config.component'; | |
55 | +import { ManageWidgetActionsComponent } from './widget/action/manage-widget-actions.component'; | |
56 | +import { WidgetActionDialogComponent } from './widget/action/widget-action-dialog.component'; | |
57 | +import { CustomActionPrettyResourcesTabsComponent } from './widget/action/custom-action-pretty-resources-tabs.component'; | |
58 | +import { CustomActionPrettyEditorComponent } from './widget/action/custom-action-pretty-editor.component'; | |
53 | 59 | |
54 | 60 | @NgModule({ |
55 | 61 | entryComponents: [ |
... | ... | @@ -64,7 +70,9 @@ import { DataKeyConfigComponent } from './widget/data-key-config.component'; |
64 | 70 | AliasesEntitySelectPanelComponent, |
65 | 71 | EntityAliasesDialogComponent, |
66 | 72 | EntityAliasDialogComponent, |
67 | - DataKeyConfigDialogComponent | |
73 | + DataKeyConfigDialogComponent, | |
74 | + LegendConfigPanelComponent, | |
75 | + WidgetActionDialogComponent | |
68 | 76 | ], |
69 | 77 | declarations: |
70 | 78 | [ |
... | ... | @@ -99,7 +107,13 @@ import { DataKeyConfigComponent } from './widget/data-key-config.component'; |
99 | 107 | EntityAliasSelectComponent, |
100 | 108 | DataKeysComponent, |
101 | 109 | DataKeyConfigComponent, |
102 | - DataKeyConfigDialogComponent | |
110 | + DataKeyConfigDialogComponent, | |
111 | + LegendConfigPanelComponent, | |
112 | + LegendConfigComponent, | |
113 | + ManageWidgetActionsComponent, | |
114 | + WidgetActionDialogComponent, | |
115 | + CustomActionPrettyResourcesTabsComponent, | |
116 | + CustomActionPrettyEditorComponent | |
103 | 117 | ], |
104 | 118 | imports: [ |
105 | 119 | CommonModule, |
... | ... | @@ -130,7 +144,12 @@ import { DataKeyConfigComponent } from './widget/data-key-config.component'; |
130 | 144 | EntityAliasSelectComponent, |
131 | 145 | DataKeysComponent, |
132 | 146 | DataKeyConfigComponent, |
133 | - DataKeyConfigDialogComponent | |
147 | + DataKeyConfigDialogComponent, | |
148 | + LegendConfigComponent, | |
149 | + ManageWidgetActionsComponent, | |
150 | + WidgetActionDialogComponent, | |
151 | + CustomActionPrettyResourcesTabsComponent, | |
152 | + CustomActionPrettyEditorComponent | |
134 | 153 | ], |
135 | 154 | providers: [ |
136 | 155 | WidgetComponentService | ... | ... |
ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-editor.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 | +<div class="tb-custom-action-pretty mat-elevation-z1" tb-fullscreen [fullscreen]="fullscreen"> | |
19 | + <div fxLayout="row" fxLayoutAlign="end center" class="tb-action-expand-button" [ngClass]="{'tb-fullscreen-editor': fullscreen}"> | |
20 | + <button mat-button fxHide.xs fxHide.sm | |
21 | + matTooltip="{{ 'widget.toggle-fullscreen' | translate }}" | |
22 | + matTooltipPosition="above" | |
23 | + (click)="fullscreen = !fullscreen"> | |
24 | + <mat-icon>{{ fullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon> | |
25 | + <span *ngIf="fullscreen" translate>widget.toggle-fullscreen</span> | |
26 | + </button> | |
27 | + </div> | |
28 | + <div class="tb-custom-action-editor" [ngClass]="{'tb-fullscreen-editor': fullscreen}"> | |
29 | + <div *ngIf="!fullscreen; else fullscreenEditor"> | |
30 | + <tb-custom-action-pretty-resources-tabs [hasCustomFunction]="true" | |
31 | + [action]="action" (actionUpdated)="onActionUpdated($event ? true : false)"> | |
32 | + </tb-custom-action-pretty-resources-tabs> | |
33 | + </div> | |
34 | + <ng-template #fullscreenEditor> | |
35 | + <div class="tb-fullscreen-panel tb-layout-fill" fxLayout="row"> | |
36 | + <div #leftPanel class="tb-split tb-content"> | |
37 | + <tb-custom-action-pretty-resources-tabs [hasCustomFunction]="false" | |
38 | + [action]="action" (actionUpdated)="onActionUpdated($event ? true : false)"> | |
39 | + </tb-custom-action-pretty-resources-tabs> | |
40 | + </div> | |
41 | + <div #rightPanel class="tb-split tb-content right-panel"> | |
42 | + <tb-js-func | |
43 | + [(ngModel)]="action.customFunction" | |
44 | + (ngModelChange)="onActionUpdated()" | |
45 | + [fillHeight]="true" | |
46 | + [functionArgs]="['$event', 'widgetContext', 'entityId', 'entityName', 'htmlTemplate', 'additionalParams']" | |
47 | + [validationArgs]="[]"> | |
48 | + </tb-js-func> | |
49 | + </div> | |
50 | + </div> | |
51 | + </ng-template> | |
52 | + </div> | |
53 | +</div> | ... | ... |
ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-editor.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 | +.tb-custom-action-pretty { | |
17 | + box-sizing: border-box; | |
18 | + position: relative; | |
19 | + padding: 8px; | |
20 | + background-color: #fff; | |
21 | + | |
22 | + .tb-fullscreen-panel { | |
23 | + .tb-custom-action-editor-container { | |
24 | + height: calc(100% - 40px); | |
25 | + } | |
26 | + | |
27 | + .right-panel { | |
28 | + padding-top: 8px; | |
29 | + padding-left: 3px; | |
30 | + } | |
31 | + | |
32 | + tb-js-func .tb-js-func-panel { | |
33 | + box-sizing: border-box; | |
34 | + } | |
35 | + | |
36 | + mat-tab-group { | |
37 | + .mat-tab-body-wrapper { | |
38 | + height: 100%; | |
39 | + mat-tab-body { | |
40 | + height: 100%; | |
41 | + & > div { | |
42 | + height: 100%; | |
43 | + } | |
44 | + } | |
45 | + } | |
46 | + } | |
47 | + } | |
48 | + | |
49 | + .tb-split { | |
50 | + box-sizing: border-box; | |
51 | + overflow-x: hidden; | |
52 | + overflow-y: auto; | |
53 | + } | |
54 | + | |
55 | + .tb-content { | |
56 | + border: 1px solid #c0c0c0; | |
57 | + } | |
58 | + | |
59 | + .gutter { | |
60 | + background-color: #eee; | |
61 | + background-repeat: no-repeat; | |
62 | + background-position: 50%; | |
63 | + } | |
64 | + | |
65 | + .gutter.gutter-horizontal { | |
66 | + cursor: col-resize; | |
67 | + background-image: url("../../../../../../assets/split.js/grips/vertical.png"); | |
68 | + } | |
69 | + | |
70 | + .tb-split.tb-split-horizontal, | |
71 | + .gutter.gutter-horizontal { | |
72 | + float: left; | |
73 | + height: 100%; | |
74 | + } | |
75 | + | |
76 | + .tb-action-expand-button { | |
77 | + position: absolute; | |
78 | + right: 14px; | |
79 | + z-index: 2; | |
80 | + | |
81 | + &.tb-fullscreen-editor { | |
82 | + position: relative; | |
83 | + right: 0; | |
84 | + .mat-button { | |
85 | + .mat-icon { | |
86 | + margin-right: 5px; | |
87 | + } | |
88 | + } | |
89 | + } | |
90 | + | |
91 | + .mat-button { | |
92 | + min-width: 36px; | |
93 | + padding: 0; | |
94 | + .mat-icon { | |
95 | + margin-right: 0; | |
96 | + } | |
97 | + } | |
98 | + } | |
99 | + | |
100 | + .tb-custom-action-editor { | |
101 | + &.tb-fullscreen-editor { | |
102 | + height: 100%; | |
103 | + } | |
104 | + } | |
105 | +} | |
106 | + | |
107 | + | ... | ... |
ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-editor.component.ts
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 | + | |
17 | +import { | |
18 | + AfterViewInit, | |
19 | + ChangeDetectionStrategy, | |
20 | + Component, | |
21 | + ElementRef, | |
22 | + forwardRef, | |
23 | + Input, | |
24 | + OnDestroy, | |
25 | + OnInit, | |
26 | + ViewChild, ViewEncapsulation, ViewChildren, QueryList, ComponentFactoryResolver | |
27 | +} from '@angular/core'; | |
28 | +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; | |
29 | +import { TranslateService } from '@ngx-translate/core'; | |
30 | +import { PageComponent } from '@shared/components/page.component'; | |
31 | +import { Store } from '@ngrx/store'; | |
32 | +import { AppState } from '@core/core.state'; | |
33 | +import { MatDialog } from '@angular/material/dialog'; | |
34 | +import { DialogService } from '@core/services/dialog.service'; | |
35 | +import { PageLink } from '@shared/models/page/page-link'; | |
36 | +import { Direction, SortOrder } from '@shared/models/page/sort-order'; | |
37 | +import { MatPaginator } from '@angular/material/paginator'; | |
38 | +import { MatSort } from '@angular/material/sort'; | |
39 | +import { combineLatest, fromEvent, merge } from 'rxjs'; | |
40 | +import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators'; | |
41 | +import { | |
42 | + WidgetActionDescriptorInfo, | |
43 | + WidgetActionsData, | |
44 | + WidgetActionsDatasource, | |
45 | + WidgetActionCallbacks, toWidgetActionDescriptor | |
46 | +} from '@home/components/widget/action/manage-widget-actions.component.models'; | |
47 | +import { UtilsService } from '@core/services/utils.service'; | |
48 | +import { EntityRelation, EntitySearchDirection, RelationTypeGroup } from '@shared/models/relation.models'; | |
49 | +import { RelationDialogComponent, RelationDialogData } from '@home/components/relation/relation-dialog.component'; | |
50 | +import { CustomActionDescriptor, WidgetActionDescriptor, WidgetActionSource } from '@shared/models/widget.models'; | |
51 | +import { | |
52 | + WidgetActionDialogComponent, | |
53 | + WidgetActionDialogData | |
54 | +} from '@home/components/widget/action/widget-action-dialog.component'; | |
55 | +import { deepClone } from '@core/utils'; | |
56 | +import { TbAnchorComponent } from '@shared/components/tb-anchor.component'; | |
57 | +import { CustomActionPrettyResourcesTabsComponent } from '@home/components/widget/action/custom-action-pretty-resources-tabs.component'; | |
58 | +import { MatTab, MatTabGroup } from '@angular/material/tabs'; | |
59 | + | |
60 | +@Component({ | |
61 | + selector: 'tb-custom-action-pretty-editor', | |
62 | + templateUrl: './custom-action-pretty-editor.component.html', | |
63 | + styleUrls: ['./custom-action-pretty-editor.component.scss'], | |
64 | + providers: [ | |
65 | + { | |
66 | + provide: NG_VALUE_ACCESSOR, | |
67 | + useExisting: forwardRef(() => CustomActionPrettyEditorComponent), | |
68 | + multi: true | |
69 | + } | |
70 | + ], | |
71 | + encapsulation: ViewEncapsulation.None | |
72 | +}) | |
73 | +export class CustomActionPrettyEditorComponent extends PageComponent implements OnInit, AfterViewInit, OnDestroy, ControlValueAccessor { | |
74 | + | |
75 | + @Input() disabled: boolean; | |
76 | + | |
77 | + action: CustomActionDescriptor; | |
78 | + | |
79 | + fullscreen = false; | |
80 | + | |
81 | + @ViewChildren('leftPanel') | |
82 | + leftPanelElmRef: QueryList<ElementRef<HTMLElement>>; | |
83 | + | |
84 | + @ViewChildren('rightPanel') | |
85 | + rightPanelElmRef: QueryList<ElementRef<HTMLElement>>; | |
86 | + | |
87 | + private propagateChange = (_: any) => {}; | |
88 | + | |
89 | + constructor(protected store: Store<AppState>) { | |
90 | + super(store); | |
91 | + } | |
92 | + | |
93 | + ngOnInit(): void { | |
94 | + } | |
95 | + | |
96 | + ngAfterViewInit(): void { | |
97 | + combineLatest(this.leftPanelElmRef.changes, this.rightPanelElmRef.changes).subscribe(() => { | |
98 | + if (this.leftPanelElmRef.length && this.rightPanelElmRef.length) { | |
99 | + this.initSplitLayout(this.leftPanelElmRef.first.nativeElement, | |
100 | + this.rightPanelElmRef.first.nativeElement); | |
101 | + } | |
102 | + }); | |
103 | + } | |
104 | + | |
105 | + private initSplitLayout(leftPanel: any, rightPanel: any) { | |
106 | + Split([leftPanel, rightPanel], { | |
107 | + sizes: [50, 50], | |
108 | + gutterSize: 8, | |
109 | + cursor: 'col-resize' | |
110 | + }); | |
111 | + } | |
112 | + | |
113 | + ngOnDestroy(): void { | |
114 | + } | |
115 | + | |
116 | + registerOnChange(fn: any): void { | |
117 | + this.propagateChange = fn; | |
118 | + } | |
119 | + | |
120 | + registerOnTouched(fn: any): void { | |
121 | + } | |
122 | + | |
123 | + setDisabledState(isDisabled: boolean): void { | |
124 | + this.disabled = isDisabled; | |
125 | + } | |
126 | + | |
127 | + writeValue(obj: CustomActionDescriptor): void { | |
128 | + this.action = obj; | |
129 | + } | |
130 | + | |
131 | + public onActionUpdated(valid: boolean = true) { | |
132 | + if (!valid) { | |
133 | + this.propagateChange(null); | |
134 | + } else { | |
135 | + this.propagateChange(this.action); | |
136 | + } | |
137 | + } | |
138 | +} | ... | ... |
1 | +<!-- | |
2 | + | |
3 | + Copyright © 2016-2019 The Thingsboard Authors | |
4 | + | |
5 | + Licensed under the Apache License, Version 2.0 (the "License"); | |
6 | + you may not use this file except in compliance with the License. | |
7 | + You may obtain a copy of the License at | |
8 | + | |
9 | + http://www.apache.org/licenses/LICENSE-2.0 | |
10 | + | |
11 | + Unless required by applicable law or agreed to in writing, software | |
12 | + distributed under the License is distributed on an "AS IS" BASIS, | |
13 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
14 | + See the License for the specific language governing permissions and | |
15 | + limitations under the License. | |
16 | + | |
17 | +--> | |
18 | +<mat-tab-group selectedIndex="{{hasCustomFunction ? 3 : 2}}" dynamicHeight="true" style="width: 100%; height: 100%;"> | |
19 | + <mat-tab label="{{ 'widget.resources' | translate }}" style="width: 100%; height: 100%;"> | |
20 | + <div class="tb-custom-action-editor-container" style="background-color: #fff;"> | |
21 | + <div class="mat-padding"> | |
22 | + <div fxFlex fxLayout="row" style="max-height: 40px;" | |
23 | + fxLayoutAlign="start center" | |
24 | + *ngFor="let resource of action.customResources; let i = index" > | |
25 | + <mat-form-field fxFlex class="mat-block resource-field" floatLabel="never" | |
26 | + style="margin: 10px 0px 0px 0px; max-height: 40px;"> | |
27 | + <input required matInput [(ngModel)]="resource.url" | |
28 | + (ngModelChange)="notifyActionUpdated()" | |
29 | + placeholder="{{ 'widget.resource-url' | translate }}"/> | |
30 | + </mat-form-field> | |
31 | + <button mat-button mat-icon-button color="primary" | |
32 | + [disabled]="isLoading$ | async" | |
33 | + type="button" | |
34 | + (click)="removeResource(i)" | |
35 | + matTooltip="{{'widget.remove-resource' | translate}}" | |
36 | + matTooltipPosition="above"> | |
37 | + <mat-icon>close</mat-icon> | |
38 | + </button> | |
39 | + </div> | |
40 | + <div style="margin-top: 6px;"> | |
41 | + <button mat-button mat-raised-button color="primary" | |
42 | + [disabled]="isLoading$ | async" | |
43 | + type="button" | |
44 | + (click)="addResource()" | |
45 | + matTooltip="{{'widget.add-resource' | translate}}" | |
46 | + matTooltipPosition="above"> | |
47 | + <span translate>action.add</span> | |
48 | + </button> | |
49 | + </div> | |
50 | + </div> | |
51 | + </div> | |
52 | + </mat-tab> | |
53 | + <mat-tab label="{{ 'widget.css' | translate }}" style="width: 100%; height: 100%;"> | |
54 | + <div class="tb-custom-action-editor-container" tb-fullscreen [fullscreen]="cssFullscreen"> | |
55 | + <div class="tb-editor-area-title-panel"> | |
56 | + <button mat-button | |
57 | + type="button" | |
58 | + (click)="beautifyCss()"> | |
59 | + {{ 'widget.tidy' | translate }} | |
60 | + </button> | |
61 | + <button mat-button | |
62 | + type="button" | |
63 | + mat-icon-button class="tb-mat-32" | |
64 | + (click)="cssFullscreen = !cssFullscreen" | |
65 | + matTooltip="{{(cssFullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}" | |
66 | + matTooltipPosition="above"> | |
67 | + <mat-icon>{{ cssFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon> | |
68 | + </button> | |
69 | + </div> | |
70 | + <div #cssInput class="css-panel"></div> | |
71 | + </div> | |
72 | + </mat-tab> | |
73 | + <mat-tab label="{{ 'widget.html' | translate }}" style="width: 100%; height: 100%;"> | |
74 | + <div class="tb-custom-action-editor-container" tb-fullscreen [fullscreen]="htmlFullscreen"> | |
75 | + <div class="tb-editor-area-title-panel"> | |
76 | + <button mat-button | |
77 | + type="button" (click)="beautifyHtml()"> | |
78 | + {{ 'widget.tidy' | translate }} | |
79 | + </button> | |
80 | + <button mat-button | |
81 | + type="button" | |
82 | + mat-icon-button class="tb-mat-32" | |
83 | + (click)="htmlFullscreen = !htmlFullscreen" | |
84 | + matTooltip="{{(htmlFullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}" | |
85 | + matTooltipPosition="above"> | |
86 | + <mat-icon>{{ htmlFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon> | |
87 | + </button> | |
88 | + </div> | |
89 | + <div #htmlInput class="html-panel"></div> | |
90 | + </div> | |
91 | + </mat-tab> | |
92 | + <mat-tab *ngIf="hasCustomFunction" label="{{ 'widget.js' | translate }}" style="width: 100%; height: 100%;"> | |
93 | + <tb-js-func | |
94 | + [(ngModel)]="action.customFunction" | |
95 | + (ngModelChange)="notifyActionUpdated()" | |
96 | + [functionArgs]="['$event', 'widgetContext', 'entityId', 'entityName', 'htmlTemplate', 'additionalParams']" | |
97 | + [validationArgs]="[]"> | |
98 | + </tb-js-func> | |
99 | + </mat-tab> | |
100 | +</mat-tab-group> | ... | ... |
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 | +.tb-custom-action-editor-container { | |
17 | + | |
18 | + mat-form-field.resource-field { | |
19 | + max-height: 40px; | |
20 | + margin: 10px 0px 0px 0px; | |
21 | + .mat-form-field-wrapper { | |
22 | + padding-bottom: 0; | |
23 | + .mat-form-field-flex { | |
24 | + max-height: 40px; | |
25 | + .mat-form-field-infix { | |
26 | + border: 0; | |
27 | + } | |
28 | + } | |
29 | + .mat-form-field-underline { | |
30 | + bottom: 0; | |
31 | + } | |
32 | + } | |
33 | + } | |
34 | + | |
35 | + .html-panel, | |
36 | + .css-panel { | |
37 | + width: 100%; | |
38 | + min-width: 200px; | |
39 | + height: 100%; | |
40 | + min-height: 200px; | |
41 | + } | |
42 | + | |
43 | + div.tb-editor-area-title-panel { | |
44 | + position: absolute; | |
45 | + top: 5px; | |
46 | + right: 20px; | |
47 | + z-index: 5; | |
48 | + font-size: .8rem; | |
49 | + font-weight: 500; | |
50 | + | |
51 | + label { | |
52 | + padding: 4px; | |
53 | + color: #00acc1; | |
54 | + text-transform: uppercase; | |
55 | + background: rgba(220, 220, 220, .35); | |
56 | + border-radius: 5px; | |
57 | + &:not(:last-child) { | |
58 | + margin-right: 4px; | |
59 | + } | |
60 | + } | |
61 | + | |
62 | + button.mat-button, button.mat-icon-button, button.mat-icon-button.tb-mat-32 { | |
63 | + align-items: center; | |
64 | + vertical-align: middle; | |
65 | + min-width: 32px; | |
66 | + min-height: 15px; | |
67 | + padding: 4px; | |
68 | + margin: 0; | |
69 | + font-size: .8rem; | |
70 | + line-height: 15px; | |
71 | + color: #7b7b7b; | |
72 | + background: rgba(220, 220, 220, .35); | |
73 | + &:not(:last-child) { | |
74 | + margin-right: 4px; | |
75 | + } | |
76 | + } | |
77 | + } | |
78 | + | |
79 | +} | ... | ... |
1 | +/// | |
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | |
3 | +/// | |
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | +/// you may not use this file except in compliance with the License. | |
6 | +/// You may obtain a copy of the License at | |
7 | +/// | |
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | |
9 | +/// | |
10 | +/// Unless required by applicable law or agreed to in writing, software | |
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | |
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | +/// See the License for the specific language governing permissions and | |
14 | +/// limitations under the License. | |
15 | +/// | |
16 | + | |
17 | +import { | |
18 | + Component, | |
19 | + ElementRef, | |
20 | + EventEmitter, | |
21 | + Input, | |
22 | + OnChanges, | |
23 | + OnDestroy, | |
24 | + OnInit, | |
25 | + Output, QueryList, | |
26 | + SimpleChanges, | |
27 | + ViewChild, ViewChildren, ViewEncapsulation | |
28 | +} from '@angular/core'; | |
29 | +import { TranslateService } from '@ngx-translate/core'; | |
30 | +import { PageComponent } from '@shared/components/page.component'; | |
31 | +import { Store } from '@ngrx/store'; | |
32 | +import { AppState } from '@core/core.state'; | |
33 | +import { CustomActionDescriptor } from '@shared/models/widget.models'; | |
34 | +import * as ace from 'ace-builds'; | |
35 | +import { CancelAnimationFrame, RafService } from '@core/services/raf.service'; | |
36 | +import { css_beautify, html_beautify } from 'js-beautify'; | |
37 | +import { MatTab } from '@angular/material/tabs'; | |
38 | +import { BehaviorSubject } from 'rxjs'; | |
39 | + | |
40 | +@Component({ | |
41 | + selector: 'tb-custom-action-pretty-resources-tabs', | |
42 | + templateUrl: './custom-action-pretty-resources-tabs.component.html', | |
43 | + styleUrls: ['./custom-action-pretty-resources-tabs.component.scss'], | |
44 | + encapsulation: ViewEncapsulation.None | |
45 | +}) | |
46 | +export class CustomActionPrettyResourcesTabsComponent extends PageComponent implements OnInit, OnChanges, OnDestroy { | |
47 | + | |
48 | + @Input() | |
49 | + action: CustomActionDescriptor; | |
50 | + | |
51 | + @Input() | |
52 | + hasCustomFunction: boolean; | |
53 | + | |
54 | + @Output() | |
55 | + actionUpdated: EventEmitter<CustomActionDescriptor> = new EventEmitter<CustomActionDescriptor>(); | |
56 | + | |
57 | + @ViewChild('htmlInput', {static: true}) | |
58 | + htmlInputElmRef: ElementRef; | |
59 | + | |
60 | + @ViewChild('cssInput', {static: true}) | |
61 | + cssInputElmRef: ElementRef; | |
62 | + | |
63 | + htmlFullscreen = false; | |
64 | + cssFullscreen = false; | |
65 | + | |
66 | + aceEditors: ace.Ace.Editor[] = []; | |
67 | + editorsResizeCafs: {[editorId: string]: CancelAnimationFrame} = {}; | |
68 | + aceResizeListeners: { element: any, resizeListener: any }[] = []; | |
69 | + htmlEditor: ace.Ace.Editor; | |
70 | + cssEditor: ace.Ace.Editor; | |
71 | + setValuesPending = false; | |
72 | + | |
73 | + constructor(protected store: Store<AppState>, | |
74 | + private translate: TranslateService, | |
75 | + private raf: RafService) { | |
76 | + super(store); | |
77 | + } | |
78 | + | |
79 | + ngOnInit(): void { | |
80 | + this.initAceEditors(); | |
81 | + if (this.setValuesPending) { | |
82 | + this.setAceEditorValues(); | |
83 | + this.setValuesPending = false; | |
84 | + } | |
85 | + } | |
86 | + | |
87 | + ngOnDestroy(): void { | |
88 | + this.aceResizeListeners.forEach((resizeListener) => { | |
89 | + // @ts-ignore | |
90 | + removeResizeListener(resizeListener.element, resizeListener.resizeListener); | |
91 | + }); | |
92 | + } | |
93 | + | |
94 | + ngOnChanges(changes: SimpleChanges): void { | |
95 | + for (const propName of Object.keys(changes)) { | |
96 | + const change = changes[propName]; | |
97 | + if (propName === 'action') { | |
98 | + if (this.aceEditors.length) { | |
99 | + this.setAceEditorValues(); | |
100 | + } else { | |
101 | + this.setValuesPending = true; | |
102 | + } | |
103 | + } | |
104 | + } | |
105 | + } | |
106 | + | |
107 | + public notifyActionUpdated() { | |
108 | + this.actionUpdated.emit(this.validate() ? this.action : null); | |
109 | + } | |
110 | + | |
111 | + private validate(): boolean { | |
112 | + for (const resource of this.action.customResources) { | |
113 | + if (!resource.url) { | |
114 | + return false; | |
115 | + } | |
116 | + } | |
117 | + return true; | |
118 | + } | |
119 | + | |
120 | + public addResource() { | |
121 | + this.action.customResources.push({url: ''}); | |
122 | + this.notifyActionUpdated(); | |
123 | + } | |
124 | + | |
125 | + public removeResource(index: number) { | |
126 | + if (index > -1) { | |
127 | + if (this.action.customResources.splice(index, 1).length > 0) { | |
128 | + this.notifyActionUpdated(); | |
129 | + } | |
130 | + } | |
131 | + } | |
132 | + | |
133 | + public beautifyCss(): void { | |
134 | + const res = css_beautify(this.action.customCss, {indent_size: 4}); | |
135 | + if (this.action.customCss !== res) { | |
136 | + this.action.customCss = res; | |
137 | + this.cssEditor.setValue(this.action.customCss ? this.action.customCss : '', -1); | |
138 | + this.notifyActionUpdated(); | |
139 | + } | |
140 | + } | |
141 | + | |
142 | + public beautifyHtml(): void { | |
143 | + const res = html_beautify(this.action.customHtml, {indent_size: 4, wrap_line_length: 60}); | |
144 | + if (this.action.customHtml !== res) { | |
145 | + this.action.customHtml = res; | |
146 | + this.htmlEditor.setValue(this.action.customHtml ? this.action.customHtml : '', -1); | |
147 | + this.notifyActionUpdated(); | |
148 | + } | |
149 | + } | |
150 | + | |
151 | + private initAceEditors() { | |
152 | + this.htmlEditor = this.createAceEditor(this.htmlInputElmRef, 'html'); | |
153 | + this.htmlEditor.on('input', () => { | |
154 | + const editorValue = this.htmlEditor.getValue(); | |
155 | + if (this.action.customHtml !== editorValue) { | |
156 | + this.action.customHtml = editorValue; | |
157 | + this.notifyActionUpdated(); | |
158 | + } | |
159 | + }); | |
160 | + this.cssEditor = this.createAceEditor(this.cssInputElmRef, 'css'); | |
161 | + this.cssEditor.on('input', () => { | |
162 | + const editorValue = this.cssEditor.getValue(); | |
163 | + if (this.action.customCss !== editorValue) { | |
164 | + this.action.customCss = editorValue; | |
165 | + this.notifyActionUpdated(); | |
166 | + } | |
167 | + }); | |
168 | + } | |
169 | + | |
170 | + private createAceEditor(editorElementRef: ElementRef, mode: string): ace.Ace.Editor { | |
171 | + const editorElement = editorElementRef.nativeElement; | |
172 | + let editorOptions: Partial<ace.Ace.EditorOptions> = { | |
173 | + mode: `ace/mode/${mode}`, | |
174 | + showGutter: true, | |
175 | + showPrintMargin: true | |
176 | + }; | |
177 | + const advancedOptions = { | |
178 | + enableSnippets: true, | |
179 | + enableBasicAutocompletion: true, | |
180 | + enableLiveAutocompletion: true | |
181 | + }; | |
182 | + editorOptions = {...editorOptions, ...advancedOptions}; | |
183 | + const aceEditor = ace.edit(editorElement, editorOptions); | |
184 | + aceEditor.session.setUseWrapMode(true); | |
185 | + this.aceEditors.push(aceEditor); | |
186 | + | |
187 | + const resizeListener = this.onAceEditorResize.bind(this, aceEditor); | |
188 | + | |
189 | + // @ts-ignore | |
190 | + addResizeListener(editorElement, resizeListener); | |
191 | + this.aceResizeListeners.push({element: editorElement, resizeListener}); | |
192 | + return aceEditor; | |
193 | + } | |
194 | + | |
195 | + private setAceEditorValues() { | |
196 | + this.htmlEditor.setValue(this.action.customHtml ? this.action.customHtml : '', -1); | |
197 | + this.cssEditor.setValue(this.action.customCss ? this.action.customCss : '', -1); | |
198 | + } | |
199 | + | |
200 | + private onAceEditorResize(aceEditor: ace.Ace.Editor) { | |
201 | + if (this.editorsResizeCafs[aceEditor.id]) { | |
202 | + this.editorsResizeCafs[aceEditor.id](); | |
203 | + delete this.editorsResizeCafs[aceEditor.id]; | |
204 | + } | |
205 | + this.editorsResizeCafs[aceEditor.id] = this.raf.raf(() => { | |
206 | + aceEditor.resize(); | |
207 | + aceEditor.renderer.updateFull(); | |
208 | + }); | |
209 | + } | |
210 | + | |
211 | +} | ... | ... |
1 | +/*=======================================================================*/ | |
2 | +/*========== There are two examples: for edit and add entity ==========*/ | |
3 | +/*=======================================================================*/ | |
4 | +/*======================== Edit entity example ========================*/ | |
5 | +/*=======================================================================*/ | |
6 | +/* | |
7 | +.edit-entity-form md-input-container { | |
8 | + padding-right: 10px; | |
9 | +} | |
10 | + | |
11 | +.edit-entity-form .boolean-value-input { | |
12 | + padding-left: 5px; | |
13 | +} | |
14 | + | |
15 | +.edit-entity-form .boolean-value-input .checkbox-label { | |
16 | + margin-bottom: 8px; | |
17 | + color: rgba(0,0,0,0.54); | |
18 | + font-size: 12px; | |
19 | +} | |
20 | + | |
21 | +.relations-list .header { | |
22 | + padding-right: 5px; | |
23 | + padding-bottom: 5px; | |
24 | + padding-left: 5px; | |
25 | +} | |
26 | + | |
27 | +.relations-list .header .cell { | |
28 | + padding-right: 5px; | |
29 | + padding-left: 5px; | |
30 | + font-size: 12px; | |
31 | + font-weight: 700; | |
32 | + color: rgba(0, 0, 0, .54); | |
33 | + white-space: nowrap; | |
34 | +} | |
35 | + | |
36 | +.relations-list .body { | |
37 | + padding-right: 5px; | |
38 | + padding-bottom: 15px; | |
39 | + padding-left: 5px; | |
40 | +} | |
41 | + | |
42 | +.relations-list .body .row { | |
43 | + padding-top: 5px; | |
44 | +} | |
45 | + | |
46 | +.relations-list .body .cell { | |
47 | + padding-right: 5px; | |
48 | + padding-left: 5px; | |
49 | +} | |
50 | + | |
51 | +.relations-list .body md-autocomplete-wrap md-input-container { | |
52 | + height: 30px; | |
53 | +} | |
54 | + | |
55 | +.relations-list .body .md-button { | |
56 | + margin: 0; | |
57 | +} | |
58 | + | |
59 | +.relations-list.old-relations tb-entity-select tb-entity-autocomplete button { | |
60 | + display: none; | |
61 | +} | |
62 | +*/ | |
63 | +/*========================================================================*/ | |
64 | +/*========================= Add entity example =========================*/ | |
65 | +/*========================================================================*/ | |
66 | +/* | |
67 | +.add-entity-form md-input-container { | |
68 | + padding-right: 10px; | |
69 | +} | |
70 | + | |
71 | +.add-entity-form .boolean-value-input { | |
72 | + padding-left: 5px; | |
73 | +} | |
74 | + | |
75 | +.add-entity-form .boolean-value-input .checkbox-label { | |
76 | + margin-bottom: 8px; | |
77 | + color: rgba(0,0,0,0.54); | |
78 | + font-size: 12px; | |
79 | +} | |
80 | + | |
81 | +.relations-list .header { | |
82 | + padding-right: 5px; | |
83 | + padding-bottom: 5px; | |
84 | + padding-left: 5px; | |
85 | +} | |
86 | + | |
87 | +.relations-list .header .cell { | |
88 | + padding-right: 5px; | |
89 | + padding-left: 5px; | |
90 | + font-size: 12px; | |
91 | + font-weight: 700; | |
92 | + color: rgba(0, 0, 0, .54); | |
93 | + white-space: nowrap; | |
94 | +} | |
95 | + | |
96 | +.relations-list .body { | |
97 | + padding-right: 5px; | |
98 | + padding-bottom: 15px; | |
99 | + padding-left: 5px; | |
100 | +} | |
101 | + | |
102 | +.relations-list .body .row { | |
103 | + padding-top: 5px; | |
104 | +} | |
105 | + | |
106 | +.relations-list .body .cell { | |
107 | + padding-right: 5px; | |
108 | + padding-left: 5px; | |
109 | +} | |
110 | + | |
111 | +.relations-list .body md-autocomplete-wrap md-input-container { | |
112 | + height: 30px; | |
113 | +} | |
114 | + | |
115 | +.relations-list .body .md-button { | |
116 | + margin: 0; | |
117 | +} | |
118 | +*/ | ... | ... |
1 | +<!--=======================================================================--> | |
2 | +<!--===== There are two example templates: for edit and add entity =====--> | |
3 | +<!--=======================================================================--> | |
4 | +<!--======================== Edit entity example ========================--> | |
5 | +<!--=======================================================================--> | |
6 | +<!-- --> | |
7 | +<!--<md-dialog aria-label="Edit entity">--> | |
8 | +<!-- <form name="editEntityForm" class="edit-entity-form" ng-submit="vm.save()">--> | |
9 | +<!-- <md-toolbar>--> | |
10 | +<!-- <div class="md-toolbar-tools">--> | |
11 | +<!-- <h2>Edit {{vm.entityType.toLowerCase()}} {{vm.entityName}}</h2>--> | |
12 | +<!-- <span flex></span>--> | |
13 | +<!-- <md-button class="md-icon-button" ng-click="vm.cancel()">--> | |
14 | +<!-- <ng-md-icon icon="close" aria-label="Close"></ng-md-icon>--> | |
15 | +<!-- </md-button>--> | |
16 | +<!-- </div>--> | |
17 | +<!-- </md-toolbar>--> | |
18 | +<!-- <md-dialog-content>--> | |
19 | +<!-- <div class="md-dialog-content">--> | |
20 | +<!-- <div layout="row">--> | |
21 | +<!-- <md-input-container flex class="md-block">--> | |
22 | +<!-- <label>Entity Name</label>--> | |
23 | +<!-- <input ng-model="vm.entityName" readonly>--> | |
24 | +<!-- </md-input-container>--> | |
25 | +<!-- <md-input-container flex class="md-block">--> | |
26 | +<!-- <label>Entity Type</label>--> | |
27 | +<!-- <input ng-model="vm.entityType" readonly>--> | |
28 | +<!-- </md-input-container>--> | |
29 | +<!-- <md-input-container flex class="md-block">--> | |
30 | +<!-- <label>Type</label>--> | |
31 | +<!-- <input ng-model="vm.type" readonly>--> | |
32 | +<!-- </md-input-container>--> | |
33 | +<!-- </div>--> | |
34 | +<!-- <div layout="row">--> | |
35 | +<!-- <md-input-container flex class="md-block">--> | |
36 | +<!-- <label>Latitude</label>--> | |
37 | +<!-- <input name="latitude" type="number" step="any" ng-model="vm.attributes.latitude">--> | |
38 | +<!-- </md-input-container>--> | |
39 | +<!-- <md-input-container flex class="md-block">--> | |
40 | +<!-- <label>Longitude</label>--> | |
41 | +<!-- <input name="longitude" type="number" step="any" ng-model="vm.attributes.longitude">--> | |
42 | +<!-- </md-input-container>--> | |
43 | +<!-- </div>--> | |
44 | +<!-- <div layout="row">--> | |
45 | +<!-- <md-input-container flex class="md-block">--> | |
46 | +<!-- <label>Address</label>--> | |
47 | +<!-- <input ng-model="vm.attributes.address">--> | |
48 | +<!-- </md-input-container>--> | |
49 | +<!-- <md-input-container flex class="md-block">--> | |
50 | +<!-- <label>Owner</label>--> | |
51 | +<!-- <input ng-model="vm.attributes.owner">--> | |
52 | +<!-- </md-input-container>--> | |
53 | +<!-- </div>--> | |
54 | +<!-- <div layout="row">--> | |
55 | +<!-- <md-input-container flex class="md-block">--> | |
56 | +<!-- <label>Integer Value</label>--> | |
57 | +<!-- <input name="integerNumber" type="number" step="1" ng-pattern="/^-?[0-9]+$/" ng-model="vm.attributes.number">--> | |
58 | +<!-- <div ng-messages="editEntityForm.integerNumber.$error">--> | |
59 | +<!-- <div ng-message="pattern">Invalid integer value.</div>--> | |
60 | +<!-- </div>--> | |
61 | +<!-- </md-input-container>--> | |
62 | +<!-- <div class="boolean-value-input" layout="column" layout-align="center start" flex>--> | |
63 | +<!-- <label class="checkbox-label">Boolean Value</label>--> | |
64 | +<!-- <md-checkbox ng-model="vm.attributes.booleanValue" style="margin-bottom: 40px;">--> | |
65 | +<!-- {{ (vm.attributes.booleanValue ? "value.true" : "value.false") | translate }}--> | |
66 | +<!-- </md-checkbox>--> | |
67 | +<!-- </div>--> | |
68 | +<!-- </div>--> | |
69 | +<!-- <div class="relations-list old-relations">--> | |
70 | +<!-- <div class="md-body-1" style="padding-bottom: 10px; color: rgba(0,0,0,0.57);">Relations</div>--> | |
71 | +<!-- <div class="body" ng-show="vm.relations.length">--> | |
72 | +<!-- <div class="row" layout="row" layout-align="start center" ng-repeat="relation in vm.relations track by $index">--> | |
73 | +<!-- <div class="md-whiteframe-1dp" flex layout="row" style="padding-left: 5px; margin-bottom: 3px;">--> | |
74 | +<!-- <div flex layout="column">--> | |
75 | +<!-- <div layout="row">--> | |
76 | +<!-- <md-input-container class="md-block" style="min-width: 100px;">--> | |
77 | +<!-- <label>Direction</label>--> | |
78 | +<!-- <md-select ng-disabled="true" required ng-model="relation.direction">--> | |
79 | +<!-- <md-option ng-repeat="direction in vm.entitySearchDirection" ng-value="direction">--> | |
80 | +<!-- {{ ("relation.search-direction." + direction) | translate}}--> | |
81 | +<!-- </md-option>--> | |
82 | +<!-- </md-select>--> | |
83 | +<!-- </md-input-container>--> | |
84 | +<!-- <tb-relation-type-autocomplete ng-disabled="true" flex class="md-block"--> | |
85 | +<!-- the-form="editEntityForm"--> | |
86 | +<!-- ng-model="relation.relationType"--> | |
87 | +<!-- tb-required="true">--> | |
88 | +<!-- </tb-relation-type-autocomplete>--> | |
89 | +<!-- </div>--> | |
90 | +<!-- <div layout="row">--> | |
91 | +<!-- <tb-entity-select flex class="md-block"--> | |
92 | +<!-- the-form="editEntityForm"--> | |
93 | +<!-- ng-disabled="true"--> | |
94 | +<!-- tb-required="true"--> | |
95 | +<!-- ng-model="relation.relatedEntity">--> | |
96 | +<!-- </tb-entity-select>--> | |
97 | +<!-- </div>--> | |
98 | +<!-- </div>--> | |
99 | +<!-- <div layout="column" layout-align="center center">--> | |
100 | +<!-- <md-button class="md-icon-button md-primary" style="width: 40px; min-width: 40px;"--> | |
101 | +<!-- ng-click="vm.removeOldRelation($index,relation)" aria-label="Remove">--> | |
102 | +<!-- <md-tooltip md-direction="top">Remove relation</md-tooltip>--> | |
103 | +<!-- <md-icon aria-label="Remove" class="material-icons">--> | |
104 | +<!-- close--> | |
105 | +<!-- </md-icon>--> | |
106 | +<!-- </md-button>--> | |
107 | +<!-- </div>--> | |
108 | +<!-- </div>--> | |
109 | +<!-- </div>--> | |
110 | +<!-- </div>--> | |
111 | +<!-- </div>--> | |
112 | +<!-- <div class="relations-list">--> | |
113 | +<!-- <div class="md-body-1" style="padding-bottom: 10px; color: rgba(0,0,0,0.57);">New Relations</div>--> | |
114 | +<!-- <div class="body" ng-show="vm.newRelations.length">--> | |
115 | +<!-- <div class="row" layout="row" layout-align="start center" ng-repeat="relation in vm.newRelations track by $index">--> | |
116 | +<!-- <div class="md-whiteframe-1dp" flex layout="row" style="padding-left: 5px; margin-bottom: 3px;">--> | |
117 | +<!-- <div flex layout="column">--> | |
118 | +<!-- <div layout="row">--> | |
119 | +<!-- <md-input-container class="md-block" style="min-width: 100px;">--> | |
120 | +<!-- <label>Direction</label>--> | |
121 | +<!-- <md-select name="direction" required ng-model="relation.direction">--> | |
122 | +<!-- <md-option ng-repeat="direction in vm.entitySearchDirection" ng-value="direction">--> | |
123 | +<!-- {{ ("relation.search-direction." + direction) | translate}}--> | |
124 | +<!-- </md-option>--> | |
125 | +<!-- </md-select>--> | |
126 | +<!-- <div ng-messages="editEntityForm.direction.$error">--> | |
127 | +<!-- <div ng-message="required">Relation direction is required.</div>--> | |
128 | +<!-- </div>--> | |
129 | +<!-- </md-input-container>--> | |
130 | +<!-- <tb-relation-type-autocomplete flex class="md-block"--> | |
131 | +<!-- the-form="editEntityForm"--> | |
132 | +<!-- ng-model="relation.relationType"--> | |
133 | +<!-- tb-required="true">--> | |
134 | +<!-- </tb-relation-type-autocomplete>--> | |
135 | +<!-- </div>--> | |
136 | +<!-- <div layout="row">--> | |
137 | +<!-- <tb-entity-select flex class="md-block"--> | |
138 | +<!-- the-form="editEntityForm"--> | |
139 | +<!-- tb-required="true"--> | |
140 | +<!-- ng-model="relation.relatedEntity">--> | |
141 | +<!-- </tb-entity-select>--> | |
142 | +<!-- </div>--> | |
143 | +<!-- </div>--> | |
144 | +<!-- <div layout="column" layout-align="center center">--> | |
145 | +<!-- <md-button class="md-icon-button md-primary" style="width: 40px; min-width: 40px;"--> | |
146 | +<!-- ng-click="vm.removeRelation($index)" aria-label="Remove">--> | |
147 | +<!-- <md-tooltip md-direction="top">Remove relation</md-tooltip>--> | |
148 | +<!-- <md-icon aria-label="Remove" class="material-icons">--> | |
149 | +<!-- close--> | |
150 | +<!-- </md-icon>--> | |
151 | +<!-- </md-button>--> | |
152 | +<!-- </div>--> | |
153 | +<!-- </div>--> | |
154 | +<!-- </div>--> | |
155 | +<!-- </div>--> | |
156 | +<!-- <div>--> | |
157 | +<!-- <md-button class="md-primary md-raised" ng-click="vm.addRelation()" aria-label="Add">--> | |
158 | +<!-- <md-tooltip md-direction="top">Add Relation</md-tooltip>--> | |
159 | +<!-- Add--> | |
160 | +<!-- </md-button>--> | |
161 | +<!-- </div> --> | |
162 | +<!-- </div>--> | |
163 | +<!-- </div>--> | |
164 | +<!-- </md-dialog-content>--> | |
165 | +<!-- <md-dialog-actions>--> | |
166 | +<!-- <md-button type="submit" ng-disabled="editEntityForm.$invalid || !editEntityForm.$dirty" class="md-raised md-primary">Save</md-button>--> | |
167 | +<!-- <md-button ng-click="vm.cancel()" class="md-primary">Cancel</md-button>--> | |
168 | +<!-- </md-dialog-actions>--> | |
169 | +<!-- </form>--> | |
170 | +<!--</md-dialog>--> | |
171 | +<!----> | |
172 | +<!--========================================================================--> | |
173 | +<!--========================= Add entity example =========================--> | |
174 | +<!--========================================================================--> | |
175 | +<!----> | |
176 | +<!--<md-dialog aria-label="Add entity">--> | |
177 | +<!-- <form name="addEntityForm" class="add-entity-form" ng-submit="vm.save()">--> | |
178 | +<!-- <md-toolbar>--> | |
179 | +<!-- <div class="md-toolbar-tools">--> | |
180 | +<!-- <h2>Add entity</h2>--> | |
181 | +<!-- <span flex></span>--> | |
182 | +<!-- <md-button class="md-icon-button" ng-click="vm.cancel()">--> | |
183 | +<!-- <ng-md-icon icon="close" aria-label="Close"></ng-md-icon>--> | |
184 | +<!-- </md-button>--> | |
185 | +<!-- </div>--> | |
186 | +<!-- </md-toolbar>--> | |
187 | +<!-- <md-dialog-content>--> | |
188 | +<!-- <div class="md-dialog-content">--> | |
189 | +<!-- <div layout="row">--> | |
190 | +<!-- <md-input-container flex class="md-block">--> | |
191 | +<!-- <label>Entity Name</label>--> | |
192 | +<!-- <input ng-model="vm.entityName" name=entityName required>--> | |
193 | +<!-- <div ng-messages="addEntityForm.entityName.$error">--> | |
194 | +<!-- <div ng-message="required">Entity name is required.</div>--> | |
195 | +<!-- </div>--> | |
196 | +<!-- </md-input-container>--> | |
197 | +<!-- <tb-entity-type-select class="md-block" style="min-width: 100px; width: 100px;"--> | |
198 | +<!-- the-form="addEntityForm"--> | |
199 | +<!-- tb-required="true"--> | |
200 | +<!-- allowed-entity-types="vm.allowedEntityTypes"--> | |
201 | +<!-- ng-model="vm.entityType">--> | |
202 | +<!-- </tb-entity-type-select>--> | |
203 | +<!-- <md-input-container flex class="md-block">--> | |
204 | +<!-- <label>Entity Subtype</label>--> | |
205 | +<!-- <input ng-model="vm.type" name=type required>--> | |
206 | +<!-- <div ng-messages="addEntityForm.type.$error">--> | |
207 | +<!-- <div ng-message="required">Entity subtype is required.</div>--> | |
208 | +<!-- </div>--> | |
209 | +<!-- </md-input-container>--> | |
210 | +<!-- </div>--> | |
211 | +<!-- <div layout="row">--> | |
212 | +<!-- <md-input-container flex class="md-block">--> | |
213 | +<!-- <label>Latitude</label>--> | |
214 | +<!-- <input name="latitude" type="number" step="any" ng-model="vm.attributes.latitude">--> | |
215 | +<!-- </md-input-container>--> | |
216 | +<!-- <md-input-container flex class="md-block">--> | |
217 | +<!-- <label>Longitude</label>--> | |
218 | +<!-- <input name="longitude" type="number" step="any" ng-model="vm.attributes.longitude">--> | |
219 | +<!-- </md-input-container>--> | |
220 | +<!-- </div>--> | |
221 | +<!-- <div layout="row">--> | |
222 | +<!-- <md-input-container flex class="md-block">--> | |
223 | +<!-- <label>Address</label>--> | |
224 | +<!-- <input ng-model="vm.attributes.address">--> | |
225 | +<!-- </md-input-container>--> | |
226 | +<!-- <md-input-container flex class="md-block">--> | |
227 | +<!-- <label>Owner</label>--> | |
228 | +<!-- <input ng-model="vm.attributes.owner">--> | |
229 | +<!-- </md-input-container>--> | |
230 | +<!-- </div>--> | |
231 | +<!-- <div layout="row">--> | |
232 | +<!-- <md-input-container flex class="md-block">--> | |
233 | +<!-- <label>Integer Value</label>--> | |
234 | +<!-- <input name="integerNumber" type="number" step="1" ng-pattern="/^-?[0-9]+$/" ng-model="vm.attributes.number">--> | |
235 | +<!-- <div ng-messages="addEntityForm.integerNumber.$error">--> | |
236 | +<!-- <div ng-message="pattern">Invalid integer value.</div>--> | |
237 | +<!-- </div>--> | |
238 | +<!-- </md-input-container>--> | |
239 | +<!-- <div class="boolean-value-input" layout="column" layout-align="center start" flex>--> | |
240 | +<!-- <label class="checkbox-label">Boolean Value</label>--> | |
241 | +<!-- <md-checkbox ng-model="vm.attributes.booleanValue" style="margin-bottom: 40px;">--> | |
242 | +<!-- {{ (vm.attributes.booleanValue ? "value.true" : "value.false") | translate }}--> | |
243 | +<!-- </md-checkbox>--> | |
244 | +<!-- </div>--> | |
245 | +<!-- </div>--> | |
246 | +<!-- <div class="relations-list">--> | |
247 | +<!-- <div class="md-body-1" style="padding-bottom: 10px; color: rgba(0,0,0,0.57);">Relations</div>--> | |
248 | +<!-- <div class="body" ng-show="vm.relations.length">--> | |
249 | +<!-- <div class="row" layout="row" layout-align="start center" ng-repeat="relation in vm.relations track by $index">--> | |
250 | +<!-- <div class="md-whiteframe-1dp" flex layout="row" style="padding-left: 5px;">--> | |
251 | +<!-- <div flex layout="column">--> | |
252 | +<!-- <div layout="row">--> | |
253 | +<!-- <md-input-container class="md-block" style="min-width: 100px;">--> | |
254 | +<!-- <label>Direction</label>--> | |
255 | +<!-- <md-select name="direction" required ng-model="relation.direction">--> | |
256 | +<!-- <md-option ng-repeat="direction in vm.entitySearchDirection" ng-value="direction">--> | |
257 | +<!-- {{ ("relation.search-direction." + direction) | translate}}--> | |
258 | +<!-- </md-option>--> | |
259 | +<!-- </md-select>--> | |
260 | +<!-- <div ng-messages="addEntityForm.direction.$error">--> | |
261 | +<!-- <div ng-message="required">Relation direction is required.</div>--> | |
262 | +<!-- </div>--> | |
263 | +<!-- </md-input-container>--> | |
264 | +<!-- <tb-relation-type-autocomplete flex class="md-block"--> | |
265 | +<!-- the-form="addEntityForm"--> | |
266 | +<!-- ng-model="relation.relationType"--> | |
267 | +<!-- tb-required="true">--> | |
268 | +<!-- </tb-relation-type-autocomplete>--> | |
269 | +<!-- </div>--> | |
270 | +<!-- <div layout="row">--> | |
271 | +<!-- <tb-entity-select flex class="md-block"--> | |
272 | +<!-- the-form="addEntityForm"--> | |
273 | +<!-- tb-required="true"--> | |
274 | +<!-- ng-model="relation.relatedEntity">--> | |
275 | +<!-- </tb-entity-select>--> | |
276 | +<!-- </div>--> | |
277 | +<!-- </div>--> | |
278 | +<!-- <div layout="column" layout-align="center center">--> | |
279 | +<!-- <md-button class="md-icon-button md-primary" style="width: 40px; min-width: 40px;"--> | |
280 | +<!-- ng-click="vm.removeRelation($index)" aria-label="Remove">--> | |
281 | +<!-- <md-tooltip md-direction="top">Remove relation</md-tooltip>--> | |
282 | +<!-- <md-icon aria-label="Remove" class="material-icons">--> | |
283 | +<!-- close--> | |
284 | +<!-- </md-icon>--> | |
285 | +<!-- </md-button>--> | |
286 | +<!-- </div>--> | |
287 | +<!-- </div>--> | |
288 | +<!-- </div>--> | |
289 | +<!-- </div>--> | |
290 | +<!-- <div>--> | |
291 | +<!-- <md-button class="md-primary md-raised" ng-click="vm.addRelation()" aria-label="Add">--> | |
292 | +<!-- <md-tooltip md-direction="top">Add Relation</md-tooltip>--> | |
293 | +<!-- Add--> | |
294 | +<!-- </md-button>--> | |
295 | +<!-- </div> --> | |
296 | +<!-- </div>--> | |
297 | +<!-- </div>--> | |
298 | +<!-- </md-dialog-content>--> | |
299 | +<!-- <md-dialog-actions>--> | |
300 | +<!-- <md-button type="submit" ng-disabled="addEntityForm.$invalid || !addEntityForm.$dirty" class="md-raised md-primary">Create</md-button>--> | |
301 | +<!-- <md-button ng-click="vm.cancel()" class="md-primary">Cancel</md-button>--> | |
302 | +<!-- </md-dialog-actions>--> | |
303 | +<!-- </form>--> | |
304 | +<!--</md-dialog>--> | ... | ... |
1 | +/*=======================================================================*/ | |
2 | +/*===== There are three examples: for delete, edit and add entity =====*/ | |
3 | +/*=======================================================================*/ | |
4 | +/*======================= Delete entity example =======================*/ | |
5 | +/*=======================================================================*/ | |
6 | +// | |
7 | +//var $injector = widgetContext.$scope.$injector; | |
8 | +//var $mdDialog = $injector.get('$mdDialog'), | |
9 | +// $document = $injector.get('$document'), | |
10 | +// types = $injector.get('types'), | |
11 | +// assetService = $injector.get('assetService'), | |
12 | +// deviceService = $injector.get('deviceService') | |
13 | +// $rootScope = $injector.get('$rootScope'), | |
14 | +// $q = $injector.get('$q'); | |
15 | +// | |
16 | +//openDeleteEntityDialog(); | |
17 | +// | |
18 | +//function openDeleteEntityDialog() { | |
19 | +// var title = 'Delete ' + entityId.entityType | |
20 | +// .toLowerCase() + ' ' + | |
21 | +// entityName; | |
22 | +// var content = 'Are you sure you want to delete the ' + | |
23 | +// entityId.entityType.toLowerCase() + ' ' + | |
24 | +// entityName + '?'; | |
25 | +// var confirm = $mdDialog.confirm() | |
26 | +// .targetEvent($event) | |
27 | +// .title(title) | |
28 | +// .htmlContent(content) | |
29 | +// .ariaLabel(title) | |
30 | +// .cancel('Cancel') | |
31 | +// .ok('Delete'); | |
32 | +// $mdDialog.show(confirm).then(function() { | |
33 | +// deleteEntity(); | |
34 | +// }) | |
35 | +//} | |
36 | +// | |
37 | +//function deleteEntity() { | |
38 | +// deleteEntityPromise(entityId).then( | |
39 | +// function success() { | |
40 | +// updateAliasData(); | |
41 | +// }, | |
42 | +// function fail() { | |
43 | +// showErrorDialog(); | |
44 | +// } | |
45 | +// ); | |
46 | +//} | |
47 | +// | |
48 | +//function deleteEntityPromise(entityId) { | |
49 | +// if (entityId.entityType == types.entityType.asset) { | |
50 | +// return assetService.deleteAsset(entityId.id); | |
51 | +// } else if (entityId.entityType == types.entityType.device) { | |
52 | +// return deviceService.deleteDevice(entityId.id); | |
53 | +// } | |
54 | +//} | |
55 | +// | |
56 | +//function updateAliasData() { | |
57 | +// var aliasIds = []; | |
58 | +// for (var id in widgetContext.aliasController.resolvedAliases) { | |
59 | +// aliasIds.push(id); | |
60 | +// } | |
61 | +// var tasks = []; | |
62 | +// aliasIds.forEach(function(aliasId) { | |
63 | +// widgetContext.aliasController.setAliasUnresolved(aliasId); | |
64 | +// tasks.push(widgetContext.aliasController.getAliasInfo(aliasId)); | |
65 | +// }); | |
66 | +// $q.all(tasks).then(function() { | |
67 | +// $rootScope.$broadcast('entityAliasesChanged', aliasIds); | |
68 | +// }); | |
69 | +//} | |
70 | +// | |
71 | +//function showErrorDialog() { | |
72 | +// var title = 'Error'; | |
73 | +// var content = 'An error occurred while deleting the entity. Please try again.'; | |
74 | +// var alert = $mdDialog.alert() | |
75 | +// .title(title) | |
76 | +// .htmlContent(content) | |
77 | +// .ariaLabel(title) | |
78 | +// .parent(angular.element($document[0].body)) | |
79 | +// .targetEvent($event) | |
80 | +// .multiple(true) | |
81 | +// .clickOutsideToClose(true) | |
82 | +// .ok('CLOSE'); | |
83 | +// $mdDialog.show(alert); | |
84 | +//} | |
85 | +// | |
86 | +/*=======================================================================*/ | |
87 | +/*======================== Edit entity example ========================*/ | |
88 | +/*=======================================================================*/ | |
89 | +// | |
90 | +//var $injector = widgetContext.$scope.$injector; | |
91 | +//var $mdDialog = $injector.get('$mdDialog'), | |
92 | +// $document = $injector.get('$document'), | |
93 | +// $q = $injector.get('$q'), | |
94 | +// types = $injector.get('types'), | |
95 | +// $rootScope = $injector.get('$rootScope'), | |
96 | +// entityService = $injector.get('entityService'), | |
97 | +// attributeService = $injector.get('attributeService'), | |
98 | +// entityRelationService = $injector.get('entityRelationService'); | |
99 | +// | |
100 | +//openEditEntityDialog(); | |
101 | +// | |
102 | +//function openEditEntityDialog() { | |
103 | +// $mdDialog.show({ | |
104 | +// controller: ['$scope','$mdDialog', EditEntityDialogController], | |
105 | +// controllerAs: 'vm', | |
106 | +// template: htmlTemplate, | |
107 | +// locals: { | |
108 | +// entityId: entityId | |
109 | +// }, | |
110 | +// parent: angular.element($document[0].body), | |
111 | +// targetEvent: $event, | |
112 | +// multiple: true, | |
113 | +// clickOutsideToClose: false | |
114 | +// }); | |
115 | +//} | |
116 | +// | |
117 | +//function EditEntityDialogController($scope,$mdDialog) { | |
118 | +// var vm = this; | |
119 | +// vm.entityId = entityId; | |
120 | +// vm.entityName = entityName; | |
121 | +// vm.entityType = entityId.entityType; | |
122 | +// vm.allowedEntityTypes = [types.entityType.asset, types.entityType.device]; | |
123 | +// vm.allowedRelatedEntityTypes = []; | |
124 | +// vm.entitySearchDirection = types.entitySearchDirection; | |
125 | +// vm.attributes = {}; | |
126 | +// vm.serverAttributes = {}; | |
127 | +// vm.relations = []; | |
128 | +// vm.newRelations = []; | |
129 | +// vm.relationsToDelete = []; | |
130 | +// getEntityInfo(); | |
131 | +// | |
132 | +// vm.addRelation = function() { | |
133 | +// var relation = { | |
134 | +// direction: null, | |
135 | +// relationType: null, | |
136 | +// relatedEntity: null | |
137 | +// }; | |
138 | +// vm.newRelations.push(relation); | |
139 | +// $scope.editEntityForm.$setDirty(); | |
140 | +// }; | |
141 | +// vm.removeRelation = function(index) { | |
142 | +// if (index > -1) { | |
143 | +// vm.newRelations.splice(index, 1); | |
144 | +// $scope.editEntityForm.$setDirty(); | |
145 | +// } | |
146 | +// }; | |
147 | +// vm.removeOldRelation = function(index, relation) { | |
148 | +// if (index > -1) { | |
149 | +// vm.relations.splice(index, 1); | |
150 | +// vm.relationsToDelete.push(relation); | |
151 | +// $scope.editEntityForm.$setDirty(); | |
152 | +// } | |
153 | +// }; | |
154 | +// vm.save = function() { | |
155 | +// saveAttributes(); | |
156 | +// saveRelations(); | |
157 | +// $scope.editEntityForm.$setPristine(); | |
158 | +// }; | |
159 | +// vm.cancel = function() { | |
160 | +// $mdDialog.hide(); | |
161 | +// }; | |
162 | +// | |
163 | +// function getEntityAttributes(attributes) { | |
164 | +// for (var i = 0; i < attributes.length; i++) { | |
165 | +// vm.attributes[attributes[i].key] = attributes[i].value; | |
166 | +// } | |
167 | +// vm.serverAttributes = angular.copy(vm.attributes); | |
168 | +// } | |
169 | +// | |
170 | +// function getEntityRelations(relations) { | |
171 | +// var relationsFrom = relations[0]; | |
172 | +// var relationsTo = relations[1]; | |
173 | +// for (var i=0; i < relationsFrom.length; i++) { | |
174 | +// var relation = { | |
175 | +// direction: types.entitySearchDirection.from, | |
176 | +// relationType: relationsFrom[i].type, | |
177 | +// relatedEntity: relationsFrom[i].to | |
178 | +// }; | |
179 | +// vm.relations.push(relation); | |
180 | +// } | |
181 | +// for (var i=0; i < relationsTo.length; i++) { | |
182 | +// var relation = { | |
183 | +// direction: types.entitySearchDirection.to, | |
184 | +// relationType: relationsTo[i].type, | |
185 | +// relatedEntity: relationsTo[i].from | |
186 | +// }; | |
187 | +// vm.relations.push(relation); | |
188 | +// } | |
189 | +// } | |
190 | +// | |
191 | +// function getEntityInfo() { | |
192 | +// entityService.getEntity(entityId.entityType, entityId.id).then( | |
193 | +// function(entity) { | |
194 | +// vm.entity = entity; | |
195 | +// vm.type = vm.entity.type; | |
196 | +// }); | |
197 | +// attributeService.getEntityAttributesValues(entityId.entityType, entityId.id, 'SERVER_SCOPE').then( | |
198 | +// function(data){ | |
199 | +// if (data.length) { | |
200 | +// getEntityAttributes(data); | |
201 | +// } | |
202 | +// }); | |
203 | +// $q.all([entityRelationService.findInfoByFrom(entityId.id, entityId.entityType), entityRelationService.findInfoByTo(entityId.id, entityId.entityType)]).then( | |
204 | +// function(relations){ | |
205 | +// getEntityRelations(relations); | |
206 | +// }); | |
207 | +// } | |
208 | +// | |
209 | +// function saveAttributes() { | |
210 | +// var attributesArray = []; | |
211 | +// for (var key in vm.attributes) { | |
212 | +// if (vm.attributes[key] !== vm.serverAttributes[key]) { | |
213 | +// attributesArray.push({key: key, value: vm.attributes[key]}); | |
214 | +// } | |
215 | +// } | |
216 | +// if (attributesArray.length > 0) { | |
217 | +// attributeService.saveEntityAttributes(entityId.entityType, entityId.id, \"SERVER_SCOPE\", attributesArray); | |
218 | +// } | |
219 | +// } | |
220 | +// | |
221 | +// function saveRelations() { | |
222 | +// var tasks = []; | |
223 | +// for (var i=0; i < vm.newRelations.length; i++) { | |
224 | +// var relation = { | |
225 | +// type: vm.newRelations[i].relationType | |
226 | +// }; | |
227 | +// if (vm.newRelations[i].direction == types.entitySearchDirection.from) { | |
228 | +// relation.to = vm.newRelations[i].relatedEntity; | |
229 | +// relation.from = entityId; | |
230 | +// } else { | |
231 | +// relation.to = entityId; | |
232 | +// relation.from = vm.newRelations[i].relatedEntity; | |
233 | +// } | |
234 | +// tasks.push(entityRelationService.saveRelation(relation)); | |
235 | +// } | |
236 | +// for (var i=0; i < vm.relationsToDelete.length; i++) { | |
237 | +// var relation = { | |
238 | +// type: vm.relationsToDelete[i].relationType | |
239 | +// }; | |
240 | +// if (vm.relationsToDelete[i].direction == types.entitySearchDirection.from) { | |
241 | +// relation.to = vm.relationsToDelete[i].relatedEntity; | |
242 | +// relation.from = entityId; | |
243 | +// } else { | |
244 | +// relation.to = entityId; | |
245 | +// relation.from = vm.relationsToDelete[i].relatedEntity; | |
246 | +// } | |
247 | +// tasks.push(entityRelationService.deleteRelation(relation.from.id, relation.from.entityType, relation.type, relation.to.id, relation.to.entityType)); | |
248 | +// } | |
249 | +// $q.all(tasks).then(function(){ | |
250 | +// vm.relations = vm.relations.concat(vm.newRelations); | |
251 | +// vm.newRelations = []; | |
252 | +// vm.relationsToDelete = []; | |
253 | +// updateAliasData(); | |
254 | +// }); | |
255 | +// } | |
256 | +// | |
257 | +// function updateAliasData() { | |
258 | +// var aliasIds = []; | |
259 | +// for (var id in widgetContext.aliasController.resolvedAliases) { | |
260 | +// aliasIds.push(id); | |
261 | +// } | |
262 | +// var tasks = []; | |
263 | +// aliasIds.forEach(function(aliasId) { | |
264 | +// widgetContext.aliasController.setAliasUnresolved(aliasId); | |
265 | +// tasks.push(widgetContext.aliasController.getAliasInfo(aliasId)); | |
266 | +// }); | |
267 | +// $q.all(tasks).then(function() { | |
268 | +// $rootScope.$broadcast('entityAliasesChanged', aliasIds); | |
269 | +// }); | |
270 | +// } | |
271 | +//} | |
272 | +// | |
273 | +/*========================================================================*/ | |
274 | +/*========================= Add entity example =========================*/ | |
275 | +/*========================================================================*/ | |
276 | +// | |
277 | +//var $injector = widgetContext.$scope.$injector; | |
278 | +//var $mdDialog = $injector.get('$mdDialog'), | |
279 | +// $document = $injector.get('$document'), | |
280 | +// $q = $injector.get('$q'), | |
281 | +// $rootScope = $injector.get('$rootScope'), | |
282 | +// types = $injector.get('types'), | |
283 | +// assetService = $injector.get('assetService'), | |
284 | +// deviceService = $injector.get('deviceService'), | |
285 | +// attributeService = $injector.get('attributeService'), | |
286 | +// entityRelationService = $injector.get('entityRelationService'); | |
287 | +// | |
288 | +//openAddEntityDialog(); | |
289 | +// | |
290 | +//function openAddEntityDialog() { | |
291 | +// $mdDialog.show({ | |
292 | +// controller: ['$scope','$mdDialog', AddEntityDialogController], | |
293 | +// controllerAs: 'vm', | |
294 | +// template: htmlTemplate, | |
295 | +// locals: { | |
296 | +// entityId: entityId | |
297 | +// }, | |
298 | +// parent: angular.element($document[0].body), | |
299 | +// targetEvent: $event, | |
300 | +// multiple: true, | |
301 | +// clickOutsideToClose: false | |
302 | +// }); | |
303 | +//} | |
304 | +// | |
305 | +//function AddEntityDialogController($scope, $mdDialog) { | |
306 | +// var vm = this; | |
307 | +// vm.allowedEntityTypes = [types.entityType.asset, types.entityType.device]; | |
308 | +// vm.allowedRelatedEntityTypes = []; | |
309 | +// vm.entitySearchDirection = types.entitySearchDirection; | |
310 | +// vm.attributes = {}; | |
311 | +// vm.relations = []; | |
312 | +// | |
313 | +// vm.addRelation = function() { | |
314 | +// var relation = { | |
315 | +// direction: null, | |
316 | +// relationType: null, | |
317 | +// relatedEntity: null | |
318 | +// }; | |
319 | +// vm.relations.push(relation); | |
320 | +// }; | |
321 | +// vm.removeRelation = function(index) { | |
322 | +// if (index > -1) { | |
323 | +// vm.relations.splice(index, 1); | |
324 | +// } | |
325 | +// }; | |
326 | +// vm.save = function() { | |
327 | +// $scope.addEntityForm.$setPristine(); | |
328 | +// saveEntityPromise().then( | |
329 | +// function (entity) { | |
330 | +// saveAttributes(entity.id); | |
331 | +// saveRelations(entity.id); | |
332 | +// $mdDialog.hide(); | |
333 | +// } | |
334 | +// ); | |
335 | +// }; | |
336 | +// vm.cancel = function() { | |
337 | +// $mdDialog.hide(); | |
338 | +// }; | |
339 | +// | |
340 | +// | |
341 | +// function saveEntityPromise() { | |
342 | +// var entity = { | |
343 | +// name: vm.entityName, | |
344 | +// type: vm.type | |
345 | +// }; | |
346 | +// if (vm.entityType == types.entityType.asset) { | |
347 | +// return assetService.saveAsset(entity); | |
348 | +// } else if (vm.entityType == types.entityType.device) { | |
349 | +// return deviceService.saveDevice(entity); | |
350 | +// } | |
351 | +// } | |
352 | +// | |
353 | +// function saveAttributes(entityId) { | |
354 | +// var attributesArray = []; | |
355 | +// for (var key in vm.attributes) { | |
356 | +// attributesArray.push({key: key, value: vm.attributes[key]}); | |
357 | +// } | |
358 | +// if (attributesArray.length > 0) { | |
359 | +// attributeService.saveEntityAttributes(entityId.entityType, entityId.id, \"SERVER_SCOPE\", attributesArray); | |
360 | +// } | |
361 | +// } | |
362 | +// | |
363 | +// function saveRelations(entityId) { | |
364 | +// var tasks = []; | |
365 | +// for (var i=0; i < vm.relations.length; i++) { | |
366 | +// var relation = { | |
367 | +// type: vm.relations[i].relationType | |
368 | +// }; | |
369 | +// if (vm.relations[i].direction == types.entitySearchDirection.from) { | |
370 | +// relation.to = vm.relations[i].relatedEntity; | |
371 | +// relation.from = entityId; | |
372 | +// } else { | |
373 | +// relation.to = entityId; | |
374 | +// relation.from = vm.relations[i].relatedEntity; | |
375 | +// } | |
376 | +// tasks.push(entityRelationService.saveRelation(relation)); | |
377 | +// } | |
378 | +// $q.all(tasks).then(function(){ | |
379 | +// updateAliasData(); | |
380 | +// }); | |
381 | +// } | |
382 | +// | |
383 | +// function updateAliasData() { | |
384 | +// var aliasIds = []; | |
385 | +// for (var id in widgetContext.aliasController.resolvedAliases) { | |
386 | +// aliasIds.push(id); | |
387 | +// } | |
388 | +// var tasks = []; | |
389 | +// aliasIds.forEach(function(aliasId) { | |
390 | +// widgetContext.aliasController.setAliasUnresolved(aliasId); | |
391 | +// tasks.push(widgetContext.aliasController.getAliasInfo(aliasId)); | |
392 | +// }); | |
393 | +// $q.all(tasks).then(function() { | |
394 | +// $rootScope.$broadcast('entityAliasesChanged', aliasIds); | |
395 | +// }); | |
396 | +// } | |
397 | +//} | ... | ... |
ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.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 | +<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]="!textSearchMode"> | |
21 | + <div class="mat-toolbar-tools"> | |
22 | + <span class="tb-entity-table-title" translate>widget-config.actions</span> | |
23 | + <span fxFlex></span> | |
24 | + <button mat-button mat-icon-button [disabled]="isLoading$ | async" | |
25 | + (click)="addAction($event)" | |
26 | + matTooltip="{{ 'widget-config.add-action' | translate }}" | |
27 | + matTooltipPosition="above"> | |
28 | + <mat-icon>add</mat-icon> | |
29 | + </button> | |
30 | + <button mat-button mat-icon-button [disabled]="isLoading$ | async" (click)="enterFilterMode()" | |
31 | + matTooltip="{{ 'action.search' | translate }}" | |
32 | + matTooltipPosition="above"> | |
33 | + <mat-icon>search</mat-icon> | |
34 | + </button> | |
35 | + </div> | |
36 | + </mat-toolbar> | |
37 | + <mat-toolbar class="mat-table-toolbar" [fxShow]="textSearchMode"> | |
38 | + <div class="mat-toolbar-tools"> | |
39 | + <button mat-button mat-icon-button | |
40 | + matTooltip="{{ 'widget-config.search-actions' | translate }}" | |
41 | + matTooltipPosition="above"> | |
42 | + <mat-icon>search</mat-icon> | |
43 | + </button> | |
44 | + <mat-form-field fxFlex> | |
45 | + <mat-label> </mat-label> | |
46 | + <input #searchInput matInput | |
47 | + [(ngModel)]="pageLink.textSearch" | |
48 | + placeholder="{{ 'widget-config.search-actions' | translate }}"/> | |
49 | + </mat-form-field> | |
50 | + <button mat-button mat-icon-button (click)="exitFilterMode()" | |
51 | + matTooltip="{{ 'action.close' | translate }}" | |
52 | + matTooltipPosition="above"> | |
53 | + <mat-icon>close</mat-icon> | |
54 | + </button> | |
55 | + </div> | |
56 | + </mat-toolbar> | |
57 | + <div fxFlex class="table-container"> | |
58 | + <mat-table [dataSource]="dataSource" | |
59 | + matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="(pageLink.sortOrder.direction + '').toLowerCase()" matSortDisableClear> | |
60 | + <ng-container matColumnDef="actionSourceName"> | |
61 | + <mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'widget-config.action-source' | translate }} </mat-header-cell> | |
62 | + <mat-cell *matCellDef="let action"> | |
63 | + {{ action.actionSourceName }} | |
64 | + </mat-cell> | |
65 | + </ng-container> | |
66 | + <ng-container matColumnDef="name"> | |
67 | + <mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'widget-config.action-name' | translate }} </mat-header-cell> | |
68 | + <mat-cell *matCellDef="let action"> | |
69 | + {{ action.name }} | |
70 | + </mat-cell> | |
71 | + </ng-container> | |
72 | + <ng-container matColumnDef="icon"> | |
73 | + <mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'widget-config.action-icon' | translate }} </mat-header-cell> | |
74 | + <mat-cell *matCellDef="let action"> | |
75 | + <mat-icon>{{ action.icon }}</mat-icon> | |
76 | + </mat-cell> | |
77 | + </ng-container> | |
78 | + <ng-container matColumnDef="typeName"> | |
79 | + <mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'widget-config.action-type' | translate }} </mat-header-cell> | |
80 | + <mat-cell *matCellDef="let action"> | |
81 | + {{ action.typeName }} | |
82 | + </mat-cell> | |
83 | + </ng-container> | |
84 | + <ng-container matColumnDef="actions" stickyEnd> | |
85 | + <mat-header-cell *matHeaderCellDef [ngStyle]="{ minWidth: '80px', maxWidth: '80px' }"> | |
86 | + </mat-header-cell> | |
87 | + <mat-cell *matCellDef="let action" [ngStyle]="{ minWidth: '80px', maxWidth: '80px' }"> | |
88 | + <div fxFlex fxLayout="row" fxLayoutAlign="end"> | |
89 | + <button mat-button mat-icon-button [disabled]="isLoading$ | async" | |
90 | + matTooltip="{{ 'widget-config.edit-action' | translate }}" | |
91 | + matTooltipPosition="above" | |
92 | + (click)="editAction($event, action)"> | |
93 | + <mat-icon>edit</mat-icon> | |
94 | + </button> | |
95 | + <button mat-button mat-icon-button [disabled]="isLoading$ | async" | |
96 | + matTooltip="{{ 'widget-config.delete-action' | translate }}" | |
97 | + matTooltipPosition="above" | |
98 | + (click)="deleteAction($event, action)"> | |
99 | + <mat-icon>delete</mat-icon> | |
100 | + </button> | |
101 | + </div> | |
102 | + </mat-cell> | |
103 | + </ng-container> | |
104 | + <mat-header-row [ngClass]="{'mat-row-select': true}" *matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row> | |
105 | + <mat-row [ngClass]="{'mat-row-select': true}" | |
106 | + *matRowDef="let action; columns: displayedColumns;"></mat-row> | |
107 | + </mat-table> | |
108 | + <span [fxShow]="dataSource.isEmpty() | async" | |
109 | + fxLayoutAlign="center center" | |
110 | + class="no-data-found" translate>{{ 'widget-config.no-actions-text' }}</span> | |
111 | + </div> | |
112 | + <mat-divider></mat-divider> | |
113 | + <mat-paginator [length]="dataSource.total() | async" | |
114 | + [pageIndex]="pageLink.page" | |
115 | + [pageSize]="pageLink.pageSize" | |
116 | + [pageSizeOptions]="[10, 20, 30]"></mat-paginator> | |
117 | + </div> | |
118 | +</div> | ... | ... |
ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.models.ts
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 | + | |
17 | +import { WidgetActionDescriptor, WidgetActionSource, | |
18 | + widgetActionTypeTranslationMap, CustomActionDescriptor } from '@app/shared/models/widget.models'; | |
19 | +import { CollectionViewer, DataSource } from '@angular/cdk/typings/collections'; | |
20 | +import { EntityRelationInfo, EntitySearchDirection } from '@shared/models/relation.models'; | |
21 | +import { BehaviorSubject, Observable, of, ReplaySubject } from 'rxjs'; | |
22 | +import { emptyPageData, PageData } from '@shared/models/page/page-data'; | |
23 | +import { SelectionModel } from '@angular/cdk/collections'; | |
24 | +import { EntityRelationService } from '@core/http/entity-relation.service'; | |
25 | +import { TranslateService } from '@ngx-translate/core'; | |
26 | +import { EntityId } from '@shared/models/id/entity-id'; | |
27 | +import { PageLink } from '@shared/models/page/page-link'; | |
28 | +import { catchError, map, publishReplay, refCount, share, take, tap } from 'rxjs/operators'; | |
29 | +import { entityTypeTranslations } from '@shared/models/entity-type.models'; | |
30 | +import { UtilsService } from '@core/services/utils.service'; | |
31 | +import { deepClone, isUndefined } from '@core/utils'; | |
32 | + | |
33 | +import * as customSampleJs from '!raw-loader!./custom-sample-js.raw'; | |
34 | +import * as customSampleCss from '!raw-loader!./custom-sample-css.raw'; | |
35 | +import * as customSampleHtml from '!raw-loader!./custom-sample-html.raw'; | |
36 | + | |
37 | +export interface WidgetActionCallbacks { | |
38 | + fetchDashboardStates: (query: string) => Array<string>; | |
39 | +} | |
40 | + | |
41 | +export interface WidgetActionsData { | |
42 | + actionsMap: {[actionSourceId: string]: Array<WidgetActionDescriptor>}; | |
43 | + actionSources: {[actionSourceId: string]: WidgetActionSource}; | |
44 | +} | |
45 | + | |
46 | +export interface WidgetActionDescriptorInfo extends WidgetActionDescriptor { | |
47 | + actionSourceId?: string; | |
48 | + actionSourceName?: string; | |
49 | + typeName?: string; | |
50 | +} | |
51 | + | |
52 | +export function toWidgetActionDescriptor(action: WidgetActionDescriptorInfo): WidgetActionDescriptor { | |
53 | + const copy = deepClone(action); | |
54 | + delete copy.actionSourceId; | |
55 | + delete copy.actionSourceName; | |
56 | + delete copy.typeName; | |
57 | + return copy; | |
58 | +} | |
59 | + | |
60 | +export function toCustomAction(action: WidgetActionDescriptorInfo): CustomActionDescriptor { | |
61 | + let result: CustomActionDescriptor; | |
62 | + if (!action || (isUndefined(action.customFunction) && isUndefined(action.customHtml) && isUndefined(action.customCss))) { | |
63 | + result = { | |
64 | + customHtml: customSampleHtml, | |
65 | + customCss: customSampleCss, | |
66 | + customFunction: customSampleJs | |
67 | + }; | |
68 | + } else { | |
69 | + result = { | |
70 | + customHtml: action.customHtml, | |
71 | + customCss: action.customCss, | |
72 | + customFunction: action.customFunction | |
73 | + }; | |
74 | + } | |
75 | + result.customResources = action ? deepClone(action.customResources) : []; | |
76 | + return result; | |
77 | +} | |
78 | + | |
79 | +export class WidgetActionsDatasource implements DataSource<WidgetActionDescriptorInfo> { | |
80 | + | |
81 | + private actionsSubject = new BehaviorSubject<WidgetActionDescriptorInfo[]>([]); | |
82 | + private pageDataSubject = new BehaviorSubject<PageData<WidgetActionDescriptorInfo>>(emptyPageData<WidgetActionDescriptorInfo>()); | |
83 | + | |
84 | + public pageData$ = this.pageDataSubject.asObservable(); | |
85 | + | |
86 | + private allActions: Observable<Array<WidgetActionDescriptorInfo>>; | |
87 | + | |
88 | + private actionsMap: {[actionSourceId: string]: Array<WidgetActionDescriptor>}; | |
89 | + private actionSources: {[actionSourceId: string]: WidgetActionSource}; | |
90 | + | |
91 | + constructor(private translate: TranslateService, | |
92 | + private utils: UtilsService) {} | |
93 | + | |
94 | + connect(collectionViewer: CollectionViewer): Observable<WidgetActionDescriptorInfo[] | ReadonlyArray<WidgetActionDescriptorInfo>> { | |
95 | + return this.actionsSubject.asObservable(); | |
96 | + } | |
97 | + | |
98 | + disconnect(collectionViewer: CollectionViewer): void { | |
99 | + this.actionsSubject.complete(); | |
100 | + this.pageDataSubject.complete(); | |
101 | + } | |
102 | + | |
103 | + setActions(actionsData: WidgetActionsData) { | |
104 | + this.actionsMap = actionsData.actionsMap; | |
105 | + this.actionSources = actionsData.actionSources; | |
106 | + } | |
107 | + | |
108 | + loadActions(pageLink: PageLink, reload: boolean = false): Observable<PageData<WidgetActionDescriptorInfo>> { | |
109 | + if (reload) { | |
110 | + this.allActions = null; | |
111 | + } | |
112 | + const result = new ReplaySubject<PageData<WidgetActionDescriptorInfo>>(); | |
113 | + this.fetchActions(pageLink).pipe( | |
114 | + catchError(() => of(emptyPageData<WidgetActionDescriptorInfo>())), | |
115 | + ).subscribe( | |
116 | + (pageData) => { | |
117 | + this.actionsSubject.next(pageData.data); | |
118 | + this.pageDataSubject.next(pageData); | |
119 | + result.next(pageData); | |
120 | + } | |
121 | + ); | |
122 | + return result; | |
123 | + } | |
124 | + | |
125 | + fetchActions(pageLink: PageLink): Observable<PageData<WidgetActionDescriptorInfo>> { | |
126 | + return this.getAllActions().pipe( | |
127 | + map((data) => pageLink.filterData(data)) | |
128 | + ); | |
129 | + } | |
130 | + | |
131 | + getAllActions(): Observable<Array<WidgetActionDescriptorInfo>> { | |
132 | + if (!this.allActions) { | |
133 | + const actions: WidgetActionDescriptorInfo[] = []; | |
134 | + for (const actionSourceId of Object.keys(this.actionsMap)) { | |
135 | + const descriptors = this.actionsMap[actionSourceId]; | |
136 | + descriptors.forEach((descriptor) => { | |
137 | + actions.push(this.toWidgetActionDescriptorInfo(actionSourceId, descriptor)); | |
138 | + }); | |
139 | + } | |
140 | + this.allActions = of(actions).pipe( | |
141 | + publishReplay(1), | |
142 | + refCount() | |
143 | + ); | |
144 | + } | |
145 | + return this.allActions; | |
146 | + } | |
147 | + | |
148 | + private toWidgetActionDescriptorInfo(actionSourceId: string, action: WidgetActionDescriptor): WidgetActionDescriptorInfo { | |
149 | + const actionSource = this.actionSources[actionSourceId]; | |
150 | + const actionSourceName = actionSource ? this.utils.customTranslation(actionSource.name, actionSource.name) : actionSourceId; | |
151 | + const typeName = this.translate.instant(widgetActionTypeTranslationMap.get(action.type)); | |
152 | + return { actionSourceId, actionSourceName, typeName, ...action}; | |
153 | + } | |
154 | + | |
155 | + isEmpty(): Observable<boolean> { | |
156 | + return this.actionsSubject.pipe( | |
157 | + map((actions) => !actions.length) | |
158 | + ); | |
159 | + } | |
160 | + | |
161 | + total(): Observable<number> { | |
162 | + return this.pageDataSubject.pipe( | |
163 | + map((pageData) => pageData.totalElements) | |
164 | + ); | |
165 | + } | |
166 | + | |
167 | +} | ... | ... |
ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.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 | + 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 | +} | ... | ... |
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 | + forwardRef, | |
23 | + Input, | |
24 | + OnDestroy, | |
25 | + OnInit, | |
26 | + ViewChild | |
27 | +} from '@angular/core'; | |
28 | +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; | |
29 | +import { TranslateService } from '@ngx-translate/core'; | |
30 | +import { PageComponent } from '@shared/components/page.component'; | |
31 | +import { Store } from '@ngrx/store'; | |
32 | +import { AppState } from '@core/core.state'; | |
33 | +import { MatDialog } from '@angular/material/dialog'; | |
34 | +import { DialogService } from '@core/services/dialog.service'; | |
35 | +import { PageLink } from '@shared/models/page/page-link'; | |
36 | +import { Direction, SortOrder } from '@shared/models/page/sort-order'; | |
37 | +import { MatPaginator } from '@angular/material/paginator'; | |
38 | +import { MatSort } from '@angular/material/sort'; | |
39 | +import { fromEvent, merge } from 'rxjs'; | |
40 | +import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators'; | |
41 | +import { | |
42 | + WidgetActionDescriptorInfo, | |
43 | + WidgetActionsData, | |
44 | + WidgetActionsDatasource, | |
45 | + WidgetActionCallbacks, toWidgetActionDescriptor | |
46 | +} from '@home/components/widget/action/manage-widget-actions.component.models'; | |
47 | +import { UtilsService } from '@core/services/utils.service'; | |
48 | +import { EntityRelation, EntitySearchDirection, RelationTypeGroup } from '@shared/models/relation.models'; | |
49 | +import { RelationDialogComponent, RelationDialogData } from '@home/components/relation/relation-dialog.component'; | |
50 | +import { WidgetActionDescriptor, WidgetActionSource } from '@shared/models/widget.models'; | |
51 | +import { | |
52 | + WidgetActionDialogComponent, | |
53 | + WidgetActionDialogData | |
54 | +} from '@home/components/widget/action/widget-action-dialog.component'; | |
55 | +import { deepClone } from '@core/utils'; | |
56 | + | |
57 | +@Component({ | |
58 | + selector: 'tb-manage-widget-actions', | |
59 | + templateUrl: './manage-widget-actions.component.html', | |
60 | + styleUrls: ['./manage-widget-actions.component.scss'], | |
61 | + providers: [ | |
62 | + { | |
63 | + provide: NG_VALUE_ACCESSOR, | |
64 | + useExisting: forwardRef(() => ManageWidgetActionsComponent), | |
65 | + multi: true | |
66 | + } | |
67 | + ] | |
68 | +}) | |
69 | +export class ManageWidgetActionsComponent extends PageComponent implements OnInit, AfterViewInit, OnDestroy, ControlValueAccessor { | |
70 | + | |
71 | + @Input() disabled: boolean; | |
72 | + | |
73 | + @Input() callbacks: WidgetActionCallbacks; | |
74 | + | |
75 | + innerValue: WidgetActionsData; | |
76 | + | |
77 | + displayedColumns: string[]; | |
78 | + pageLink: PageLink; | |
79 | + textSearchMode = false; | |
80 | + dataSource: WidgetActionsDatasource; | |
81 | + | |
82 | + viewsInited = false; | |
83 | + dirtyValue = false; | |
84 | + | |
85 | + @ViewChild('searchInput', {static: false}) searchInputField: ElementRef; | |
86 | + | |
87 | + @ViewChild(MatPaginator, {static: false}) paginator: MatPaginator; | |
88 | + @ViewChild(MatSort, {static: false}) sort: MatSort; | |
89 | + | |
90 | + private propagateChange = (_: any) => {}; | |
91 | + | |
92 | + constructor(protected store: Store<AppState>, | |
93 | + private translate: TranslateService, | |
94 | + private utils: UtilsService, | |
95 | + private dialog: MatDialog, | |
96 | + private dialogs: DialogService) { | |
97 | + super(store); | |
98 | + const sortOrder: SortOrder = { property: 'actionSourceName', direction: Direction.ASC }; | |
99 | + this.pageLink = new PageLink(10, 0, null, sortOrder); | |
100 | + this.dataSource = new WidgetActionsDatasource(this.translate, this.utils); | |
101 | + this.displayedColumns = ['actionSourceName', 'name', 'icon', 'typeName', 'actions']; | |
102 | + } | |
103 | + | |
104 | + ngOnInit(): void { | |
105 | + } | |
106 | + | |
107 | + ngOnDestroy(): void { | |
108 | + } | |
109 | + | |
110 | + ngAfterViewInit() { | |
111 | + | |
112 | + fromEvent(this.searchInputField.nativeElement, 'keyup') | |
113 | + .pipe( | |
114 | + debounceTime(150), | |
115 | + distinctUntilChanged(), | |
116 | + tap(() => { | |
117 | + this.paginator.pageIndex = 0; | |
118 | + this.updateData(); | |
119 | + }) | |
120 | + ) | |
121 | + .subscribe(); | |
122 | + | |
123 | + this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0); | |
124 | + | |
125 | + merge(this.sort.sortChange, this.paginator.page) | |
126 | + .pipe( | |
127 | + tap(() => this.updateData()) | |
128 | + ) | |
129 | + .subscribe(); | |
130 | + | |
131 | + this.viewsInited = true; | |
132 | + if (this.dirtyValue) { | |
133 | + this.dirtyValue = false; | |
134 | + this.updateData(true); | |
135 | + } | |
136 | + | |
137 | + } | |
138 | + | |
139 | + updateData(reload: boolean = false) { | |
140 | + this.pageLink.page = this.paginator.pageIndex; | |
141 | + this.pageLink.pageSize = this.paginator.pageSize; | |
142 | + this.pageLink.sortOrder.property = this.sort.active; | |
143 | + this.pageLink.sortOrder.direction = Direction[this.sort.direction.toUpperCase()]; | |
144 | + this.dataSource.loadActions(this.pageLink, reload); | |
145 | + } | |
146 | + | |
147 | + addAction($event: Event) { | |
148 | + this.openWidgetActionDialog($event); | |
149 | + } | |
150 | + | |
151 | + editAction($event: Event, action: WidgetActionDescriptorInfo) { | |
152 | + this.openWidgetActionDialog($event, action); | |
153 | + } | |
154 | + | |
155 | + openWidgetActionDialog($event: Event, action: WidgetActionDescriptorInfo = null) { | |
156 | + if ($event) { | |
157 | + $event.stopPropagation(); | |
158 | + } | |
159 | + const isAdd = action === null; | |
160 | + let prevActionSourceId = null; | |
161 | + if (!isAdd) { | |
162 | + prevActionSourceId = action.actionSourceId; | |
163 | + } | |
164 | + const availableActionSources: {[actionSourceId: string]: WidgetActionSource} = {}; | |
165 | + for (const id of Object.keys(this.innerValue.actionSources)) { | |
166 | + const actionSource = this.innerValue.actionSources[id]; | |
167 | + if (actionSource.multiple) { | |
168 | + availableActionSources[id] = actionSource; | |
169 | + } else { | |
170 | + if (!isAdd && action.actionSourceId === id) { | |
171 | + availableActionSources[id] = actionSource; | |
172 | + } else { | |
173 | + const existing = this.innerValue.actionsMap[id]; | |
174 | + if (!existing || !existing.length) { | |
175 | + availableActionSources[id] = actionSource; | |
176 | + } | |
177 | + } | |
178 | + } | |
179 | + } | |
180 | + | |
181 | + const actionsData: WidgetActionsData = { | |
182 | + actionsMap: this.innerValue.actionsMap, | |
183 | + actionSources: availableActionSources | |
184 | + }; | |
185 | + | |
186 | + this.dialog.open<WidgetActionDialogComponent, WidgetActionDialogData, | |
187 | + WidgetActionDescriptorInfo>(WidgetActionDialogComponent, { | |
188 | + disableClose: true, | |
189 | + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], | |
190 | + data: { | |
191 | + isAdd, | |
192 | + callbacks: this.callbacks, | |
193 | + actionsData, | |
194 | + action: deepClone(action) | |
195 | + } | |
196 | + }).afterClosed().subscribe( | |
197 | + (res) => { | |
198 | + if (res) { | |
199 | + this.saveAction(res, isAdd, prevActionSourceId); | |
200 | + } | |
201 | + } | |
202 | + ); | |
203 | + } | |
204 | + | |
205 | + private saveAction(actionInfo: WidgetActionDescriptorInfo, isAdd: boolean, prevActionSourceId: string) { | |
206 | + const actionSourceId = actionInfo.actionSourceId; | |
207 | + const action = toWidgetActionDescriptor(actionInfo); | |
208 | + if (isAdd) { | |
209 | + const targetActions = this.getOrCreateTargetActions(actionSourceId); | |
210 | + targetActions.push(action); | |
211 | + } else { | |
212 | + if (actionSourceId !== prevActionSourceId) { | |
213 | + let targetActions = this.getOrCreateTargetActions(prevActionSourceId); | |
214 | + const targetIndex = targetActions.findIndex((targetAction) => targetAction.id === action.id); | |
215 | + if (targetIndex > -1) { | |
216 | + targetActions.splice(targetIndex, 1); | |
217 | + } | |
218 | + targetActions = this.getOrCreateTargetActions(actionSourceId); | |
219 | + targetActions.push(action); | |
220 | + } else { | |
221 | + const targetActions = this.getOrCreateTargetActions(actionSourceId); | |
222 | + const targetIndex = targetActions.findIndex((targetAction) => targetAction.id === action.id); | |
223 | + if (targetIndex > -1) { | |
224 | + targetActions[targetIndex] = action; | |
225 | + } | |
226 | + } | |
227 | + } | |
228 | + this.onActionsUpdated(); | |
229 | + } | |
230 | + | |
231 | + private getOrCreateTargetActions(actionSourceId: string): Array<WidgetActionDescriptor> { | |
232 | + const actionsMap = this.innerValue.actionsMap; | |
233 | + let targetActions = actionsMap[actionSourceId]; | |
234 | + if (!targetActions) { | |
235 | + targetActions = []; | |
236 | + actionsMap[actionSourceId] = targetActions; | |
237 | + } | |
238 | + return targetActions; | |
239 | + } | |
240 | + | |
241 | + deleteAction($event: Event, action: WidgetActionDescriptorInfo) { | |
242 | + if ($event) { | |
243 | + $event.stopPropagation(); | |
244 | + } | |
245 | + const title = this.translate.instant('widget-config.delete-action-title'); | |
246 | + const content = this.translate.instant('widget-config.delete-action-text', {actionName: action.name}); | |
247 | + this.dialogs.confirm(title, content, | |
248 | + this.translate.instant('action.no'), | |
249 | + this.translate.instant('action.yes'), true).subscribe( | |
250 | + (res) => { | |
251 | + if (res) { | |
252 | + const targetActions = this.getOrCreateTargetActions(action.actionSourceId); | |
253 | + const targetIndex = targetActions.findIndex((targetAction) => targetAction.id === action.id); | |
254 | + if (targetIndex > -1) { | |
255 | + targetActions.splice(targetIndex, 1); | |
256 | + this.onActionsUpdated(); | |
257 | + } | |
258 | + } | |
259 | + }); | |
260 | + } | |
261 | + | |
262 | + enterFilterMode() { | |
263 | + this.textSearchMode = true; | |
264 | + this.pageLink.textSearch = ''; | |
265 | + setTimeout(() => { | |
266 | + this.searchInputField.nativeElement.focus(); | |
267 | + this.searchInputField.nativeElement.setSelectionRange(0, 0); | |
268 | + }, 10); | |
269 | + } | |
270 | + | |
271 | + exitFilterMode() { | |
272 | + this.textSearchMode = false; | |
273 | + this.pageLink.textSearch = null; | |
274 | + this.paginator.pageIndex = 0; | |
275 | + this.updateData(); | |
276 | + } | |
277 | + | |
278 | + resetSortAndFilter(update: boolean = true) { | |
279 | + this.pageLink.textSearch = null; | |
280 | + this.paginator.pageIndex = 0; | |
281 | + const sortable = this.sort.sortables.get('actionSourceName'); | |
282 | + this.sort.active = sortable.id; | |
283 | + this.sort.direction = 'asc'; | |
284 | + if (update) { | |
285 | + this.updateData(true); | |
286 | + } | |
287 | + } | |
288 | + | |
289 | + registerOnChange(fn: any): void { | |
290 | + this.propagateChange = fn; | |
291 | + } | |
292 | + | |
293 | + registerOnTouched(fn: any): void { | |
294 | + } | |
295 | + | |
296 | + setDisabledState(isDisabled: boolean): void { | |
297 | + this.disabled = isDisabled; | |
298 | + } | |
299 | + | |
300 | + writeValue(obj: WidgetActionsData): void { | |
301 | + this.innerValue = obj; | |
302 | + setTimeout(() => { | |
303 | + this.dataSource.setActions(this.innerValue); | |
304 | + if (this.viewsInited) { | |
305 | + this.resetSortAndFilter(true); | |
306 | + } else { | |
307 | + this.dirtyValue = true; | |
308 | + } | |
309 | + }, 0); | |
310 | + } | |
311 | + | |
312 | + private onActionsUpdated() { | |
313 | + this.updateData(true); | |
314 | + this.propagateChange(this.innerValue); | |
315 | + } | |
316 | +} | ... | ... |
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 #widgetActionForm="ngForm" [formGroup]="widgetActionFormGroup" (ngSubmit)="save()" style="min-width: 600px;"> | |
19 | + <mat-toolbar fxLayout="row" color="primary"> | |
20 | + <h2>{{ (isAdd ? 'widget-config.add-action' : 'widget-config.edit-action' ) | 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>widget-config.action-source</mat-label> | |
35 | + <mat-select required matInput formControlName="actionSourceId"> | |
36 | + <mat-option *ngFor="let actionSourceItem of data.actionsData.actionSources | keyvalue" [value]="actionSourceItem.key"> | |
37 | + {{ actionSourceName(actionSourceItem.value) }} | |
38 | + </mat-option> | |
39 | + </mat-select> | |
40 | + <mat-error *ngIf="widgetActionFormGroup.get('actionSourceId').hasError('required')"> | |
41 | + {{ 'widget-config.action-source-required' | translate }} | |
42 | + </mat-error> | |
43 | + </mat-form-field> | |
44 | + <mat-form-field class="mat-block" style="padding-bottom: 20px;"> | |
45 | + <mat-label translate>widget-config.action-name</mat-label> | |
46 | + <input required matInput formControlName="name"> | |
47 | + <mat-error *ngIf="widgetActionFormGroup.get('name').hasError('required')"> | |
48 | + {{ 'widget-config.action-name-required' | translate }} | |
49 | + </mat-error> | |
50 | + <mat-error *ngIf="widgetActionFormGroup.get('name').hasError('actionNameNotUnique')" | |
51 | + [innerHTML]="'widget-config.action-name-not-unique' | translate"> | |
52 | + </mat-error> | |
53 | + </mat-form-field> | |
54 | + <tb-material-icon-select | |
55 | + formControlName="icon"> | |
56 | + </tb-material-icon-select> | |
57 | + <mat-form-field class="mat-block"> | |
58 | + <mat-label translate>widget-config.action-type</mat-label> | |
59 | + <mat-select required matInput formControlName="type"> | |
60 | + <mat-option *ngFor="let actionType of widgetActionTypes" [value]="actionType"> | |
61 | + {{ widgetActionTypeTranslations.get(actionType) | translate }} | |
62 | + </mat-option> | |
63 | + </mat-select> | |
64 | + <mat-error *ngIf="widgetActionFormGroup.get('type').hasError('required')"> | |
65 | + {{ 'widget-config.action-type-required' | translate }} | |
66 | + </mat-error> | |
67 | + </mat-form-field> | |
68 | + <section fxLayout="column" [formGroup]="actionTypeFormGroup" [ngSwitch]="widgetActionFormGroup.get('type').value"> | |
69 | + <ng-template [ngSwitchCase]="widgetActionType.openDashboard"> | |
70 | + <div fxLayout="column" style="padding-bottom: 20px;"> | |
71 | + <div class="mat-caption tb-required" | |
72 | + style="padding-left: 3px; padding-bottom: 10px; color: rgba(0,0,0,0.57);" translate>widget-action.target-dashboard</div> | |
73 | + <tb-dashboard-autocomplete | |
74 | + formControlName="targetDashboardId" | |
75 | + required | |
76 | + [selectFirstDashboard]="true" | |
77 | + ></tb-dashboard-autocomplete> | |
78 | + </div> | |
79 | + </ng-template> | |
80 | + <ng-template [ngSwitchCase]="widgetActionFormGroup.get('type').value === widgetActionType.openDashboardState || | |
81 | + widgetActionFormGroup.get('type').value === widgetActionType.updateDashboardState || | |
82 | + widgetActionFormGroup.get('type').value === widgetActionType.openDashboard ? | |
83 | + widgetActionFormGroup.get('type').value : ''"> | |
84 | + <mat-form-field class="mat-block"> | |
85 | + <input matInput type="text" placeholder="{{ 'widget-action.target-dashboard-state' | translate }}" | |
86 | + #dashboardStateInput | |
87 | + formControlName="targetDashboardStateId" | |
88 | + [required]="widgetActionFormGroup.get('type').value === widgetActionType.openDashboardState" | |
89 | + [matAutocomplete]="targetDashboardStateAutocomplete"> | |
90 | + <button *ngIf="actionTypeFormGroup.get('targetDashboardStateId').value" | |
91 | + type="button" | |
92 | + matSuffix mat-button mat-icon-button aria-label="Clear" | |
93 | + (click)="clearTargetDashboardState()"> | |
94 | + <mat-icon class="material-icons">close</mat-icon> | |
95 | + </button> | |
96 | + <mat-autocomplete | |
97 | + class="tb-autocomplete" | |
98 | + #targetDashboardStateAutocomplete="matAutocomplete"> | |
99 | + <mat-option *ngFor="let state of filteredDashboardStates | async" [value]="state"> | |
100 | + <span [innerHTML]="state | highlight:targetDashboardStateSearchText"></span> | |
101 | + </mat-option> | |
102 | + </mat-autocomplete> | |
103 | + <mat-error *ngIf="actionTypeFormGroup.get('targetDashboardStateId').hasError('required')"> | |
104 | + {{ 'widget-action.target-dashboard-state-required' | translate }} | |
105 | + </mat-error> | |
106 | + </mat-form-field> | |
107 | + </ng-template> | |
108 | + <ng-template [ngSwitchCase]="widgetActionFormGroup.get('type').value === widgetActionType.openDashboardState || | |
109 | + widgetActionFormGroup.get('type').value === widgetActionType.updateDashboardState ? | |
110 | + widgetActionFormGroup.get('type').value : ''"> | |
111 | + <mat-checkbox formControlName="openRightLayout"> | |
112 | + {{ 'widget-action.open-right-layout' | translate }} | |
113 | + </mat-checkbox> | |
114 | + </ng-template> | |
115 | + <ng-template [ngSwitchCase]="widgetActionFormGroup.get('type').value === widgetActionType.openDashboardState || | |
116 | + widgetActionFormGroup.get('type').value === widgetActionType.updateDashboardState || | |
117 | + widgetActionFormGroup.get('type').value === widgetActionType.openDashboard ? | |
118 | + widgetActionFormGroup.get('type').value : ''"> | |
119 | + <div fxFlex fxLayout="column"> | |
120 | + <mat-checkbox formControlName="setEntityId"> | |
121 | + {{ 'widget-action.set-entity-from-widget' | translate }} | |
122 | + </mat-checkbox> | |
123 | + <mat-form-field *ngIf="actionTypeFormGroup.get('setEntityId').value" | |
124 | + floatLabel="always" | |
125 | + class="mat-block"> | |
126 | + <mat-label translate>alias.state-entity-parameter-name</mat-label> | |
127 | + <input matInput | |
128 | + placeholder="{{ 'alias.default-entity-parameter-name' | translate }}" | |
129 | + formControlName="stateEntityParamName"> | |
130 | + </mat-form-field> | |
131 | + </div> | |
132 | + </ng-template> | |
133 | + <ng-template [ngSwitchCase]="widgetActionType.custom"> | |
134 | + <tb-js-func | |
135 | + formControlName="customFunction" | |
136 | + [functionArgs]="['$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams']" | |
137 | + [validationArgs]="[]" | |
138 | + ></tb-js-func> | |
139 | + </ng-template> | |
140 | + <ng-template [ngSwitchCase]="widgetActionType.customPretty"> | |
141 | + <tb-custom-action-pretty-editor | |
142 | + formControlName="customAction"> | |
143 | + </tb-custom-action-pretty-editor> | |
144 | + </ng-template> | |
145 | + </section> | |
146 | + </fieldset> | |
147 | + </div> | |
148 | + <div mat-dialog-actions fxLayout="row"> | |
149 | + <span fxFlex></span> | |
150 | + <button mat-button mat-raised-button color="primary" | |
151 | + type="submit" | |
152 | + [disabled]="(isLoading$ | async) || widgetActionFormGroup.invalid || actionTypeFormGroup.invalid || (!widgetActionFormGroup.dirty && !actionTypeFormGroup.dirty)"> | |
153 | + {{ (isAdd ? 'action.add' : 'action.save') | translate }} | |
154 | + </button> | |
155 | + <button mat-button color="primary" | |
156 | + style="margin-right: 20px;" | |
157 | + type="button" | |
158 | + [disabled]="(isLoading$ | async)" | |
159 | + (click)="cancel()" cdkFocusInitial> | |
160 | + {{ 'action.cancel' | translate }} | |
161 | + </button> | |
162 | + </div> | |
163 | +</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, ElementRef, 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 { | |
22 | + FormBuilder, | |
23 | + FormControl, | |
24 | + FormGroup, | |
25 | + FormGroupDirective, | |
26 | + NgForm, | |
27 | + ValidatorFn, | |
28 | + Validators | |
29 | +} from '@angular/forms'; | |
30 | +import { Observable, of } from 'rxjs'; | |
31 | +import { Router } from '@angular/router'; | |
32 | +import { DialogComponent } from '@app/shared/components/dialog.component'; | |
33 | +import { | |
34 | + toCustomAction, | |
35 | + WidgetActionCallbacks, | |
36 | + WidgetActionDescriptorInfo, | |
37 | + WidgetActionsData | |
38 | +} from '@home/components/widget/action/manage-widget-actions.component.models'; | |
39 | +import { UtilsService } from '@core/services/utils.service'; | |
40 | +import { WidgetActionSource, WidgetActionType, widgetActionTypeTranslationMap } from '@shared/models/widget.models'; | |
41 | +import { map, mergeMap, startWith, tap } from 'rxjs/operators'; | |
42 | +import { DashboardService } from '@core/http/dashboard.service'; | |
43 | +import { Dashboard } from '@shared/models/dashboard.models'; | |
44 | +import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; | |
45 | + | |
46 | +export interface WidgetActionDialogData { | |
47 | + isAdd: boolean; | |
48 | + callbacks: WidgetActionCallbacks; | |
49 | + actionsData: WidgetActionsData; | |
50 | + action?: WidgetActionDescriptorInfo; | |
51 | +} | |
52 | + | |
53 | +@Component({ | |
54 | + selector: 'tb-widget-action-dialog', | |
55 | + templateUrl: './widget-action-dialog.component.html', | |
56 | + providers: [{provide: ErrorStateMatcher, useExisting: WidgetActionDialogComponent}], | |
57 | + styleUrls: [] | |
58 | +}) | |
59 | +export class WidgetActionDialogComponent extends DialogComponent<WidgetActionDialogComponent, | |
60 | + WidgetActionDescriptorInfo> implements OnInit, ErrorStateMatcher { | |
61 | + | |
62 | + @ViewChild('dashboardStateInput', {static: false}) dashboardStateInput: ElementRef; | |
63 | + | |
64 | + widgetActionFormGroup: FormGroup; | |
65 | + actionTypeFormGroup: FormGroup; | |
66 | + | |
67 | + isAdd: boolean; | |
68 | + action: WidgetActionDescriptorInfo; | |
69 | + | |
70 | + widgetActionTypes = Object.keys(WidgetActionType); | |
71 | + widgetActionTypeTranslations = widgetActionTypeTranslationMap; | |
72 | + widgetActionType = WidgetActionType; | |
73 | + | |
74 | + filteredDashboardStates: Observable<Array<string>>; | |
75 | + targetDashboardStateSearchText = ''; | |
76 | + selectedDashboardStateIds: Observable<Array<string>>; | |
77 | + | |
78 | + submitted = false; | |
79 | + | |
80 | + constructor(protected store: Store<AppState>, | |
81 | + protected router: Router, | |
82 | + private utils: UtilsService, | |
83 | + private dashboardService: DashboardService, | |
84 | + private dashboardUtils: DashboardUtilsService, | |
85 | + @Inject(MAT_DIALOG_DATA) public data: WidgetActionDialogData, | |
86 | + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, | |
87 | + public dialogRef: MatDialogRef<WidgetActionDialogComponent, WidgetActionDescriptorInfo>, | |
88 | + public fb: FormBuilder) { | |
89 | + super(store, router, dialogRef); | |
90 | + this.isAdd = data.isAdd; | |
91 | + if (this.isAdd) { | |
92 | + this.action = { | |
93 | + id: this.utils.guid(), | |
94 | + name: '', | |
95 | + icon: 'more_horiz', | |
96 | + type: null | |
97 | + }; | |
98 | + } else { | |
99 | + this.action = this.data.action; | |
100 | + } | |
101 | + } | |
102 | + | |
103 | + ngOnInit(): void { | |
104 | + this.widgetActionFormGroup = this.fb.group({}); | |
105 | + this.widgetActionFormGroup.addControl('actionSourceId', | |
106 | + this.fb.control(this.action.actionSourceId, [Validators.required])); | |
107 | + this.widgetActionFormGroup.addControl('name', | |
108 | + this.fb.control(this.action.name, [this.validateActionName(), Validators.required])); | |
109 | + this.widgetActionFormGroup.addControl('icon', | |
110 | + this.fb.control(this.action.icon, [Validators.required])); | |
111 | + this.widgetActionFormGroup.addControl('type', | |
112 | + this.fb.control(this.action.type, [Validators.required])); | |
113 | + this.updateActionTypeFormGroup(this.action.type, this.action); | |
114 | + this.widgetActionFormGroup.get('type').valueChanges.subscribe((type: WidgetActionType) => { | |
115 | + this.updateActionTypeFormGroup(type); | |
116 | + }); | |
117 | + this.widgetActionFormGroup.get('actionSourceId').valueChanges.subscribe(() => { | |
118 | + this.widgetActionFormGroup.get('name').updateValueAndValidity(); | |
119 | + }); | |
120 | + } | |
121 | + | |
122 | + private updateActionTypeFormGroup(type?: WidgetActionType, action?: WidgetActionDescriptorInfo) { | |
123 | + this.actionTypeFormGroup = this.fb.group({}); | |
124 | + if (type) { | |
125 | + switch (type) { | |
126 | + case WidgetActionType.openDashboard: | |
127 | + case WidgetActionType.openDashboardState: | |
128 | + case WidgetActionType.updateDashboardState: | |
129 | + this.actionTypeFormGroup.addControl( | |
130 | + 'targetDashboardStateId', | |
131 | + this.fb.control(action ? action.targetDashboardStateId : null, | |
132 | + type === WidgetActionType.openDashboardState ? [Validators.required] : []) | |
133 | + ); | |
134 | + this.actionTypeFormGroup.addControl( | |
135 | + 'setEntityId', | |
136 | + this.fb.control(action ? action.setEntityId : true, []) | |
137 | + ); | |
138 | + this.actionTypeFormGroup.addControl( | |
139 | + 'stateEntityParamName', | |
140 | + this.fb.control(action ? action.stateEntityParamName : null, []) | |
141 | + ); | |
142 | + if (type === WidgetActionType.openDashboard) { | |
143 | + this.actionTypeFormGroup.addControl( | |
144 | + 'targetDashboardId', | |
145 | + this.fb.control(action ? action.targetDashboardId : null, | |
146 | + [Validators.required]) | |
147 | + ); | |
148 | + this.setupSelectedDashboardStateIds(action ? action.targetDashboardId : null); | |
149 | + } else { | |
150 | + this.actionTypeFormGroup.addControl( | |
151 | + 'openRightLayout', | |
152 | + this.fb.control(action ? action.openRightLayout : false, []) | |
153 | + ); | |
154 | + } | |
155 | + this.setupFilteredDashboardStates(); | |
156 | + break; | |
157 | + case WidgetActionType.custom: | |
158 | + this.actionTypeFormGroup.addControl( | |
159 | + 'customFunction', | |
160 | + this.fb.control(action ? action.customFunction : null, []) | |
161 | + ); | |
162 | + break; | |
163 | + case WidgetActionType.customPretty: | |
164 | + this.actionTypeFormGroup.addControl( | |
165 | + 'customAction', | |
166 | + this.fb.control(toCustomAction(action), [Validators.required]) | |
167 | + ); | |
168 | + break; | |
169 | + } | |
170 | + } | |
171 | + } | |
172 | + | |
173 | + private setupSelectedDashboardStateIds(targetDashboardId?: string) { | |
174 | + this.selectedDashboardStateIds = | |
175 | + this.actionTypeFormGroup.get('targetDashboardId').valueChanges.pipe( | |
176 | + // startWith<string>(targetDashboardId), | |
177 | + tap(() => { | |
178 | + this.targetDashboardStateSearchText = ''; | |
179 | + }), | |
180 | + mergeMap((dashboardId) => { | |
181 | + if (dashboardId) { | |
182 | + return this.dashboardService.getDashboard(dashboardId); | |
183 | + } else { | |
184 | + return of(null); | |
185 | + } | |
186 | + }), | |
187 | + map((dashboard: Dashboard) => { | |
188 | + if (dashboard) { | |
189 | + dashboard = this.dashboardUtils.validateAndUpdateDashboard(dashboard); | |
190 | + const states = dashboard.configuration.states; | |
191 | + return Object.keys(states); | |
192 | + } else { | |
193 | + return []; | |
194 | + } | |
195 | + }) | |
196 | + ); | |
197 | + } | |
198 | + | |
199 | + private setupFilteredDashboardStates() { | |
200 | + this.targetDashboardStateSearchText = ''; | |
201 | + this.filteredDashboardStates = this.actionTypeFormGroup.get('targetDashboardStateId').valueChanges | |
202 | + .pipe( | |
203 | + startWith(''), | |
204 | + map(value => value ? value : ''), | |
205 | + mergeMap(name => this.fetchDashboardStates(name) ) | |
206 | + ); | |
207 | + } | |
208 | + | |
209 | + private fetchDashboardStates(searchText?: string): Observable<Array<string>> { | |
210 | + this.targetDashboardStateSearchText = searchText; | |
211 | + if (this.widgetActionFormGroup.get('type').value === WidgetActionType.openDashboard) { | |
212 | + return this.selectedDashboardStateIds.pipe( | |
213 | + map(stateIds => { | |
214 | + const result = searchText ? stateIds.filter(this.createFilterForDashboardState(searchText)) : stateIds; | |
215 | + if (result && result.length) { | |
216 | + return result; | |
217 | + } else { | |
218 | + return [searchText]; | |
219 | + } | |
220 | + }) | |
221 | + ); | |
222 | + } else { | |
223 | + return of(this.data.callbacks.fetchDashboardStates(searchText)); | |
224 | + } | |
225 | + } | |
226 | + | |
227 | + private createFilterForDashboardState(query: string): (stateId: string) => boolean { | |
228 | + const lowercaseQuery = query.toLowerCase(); | |
229 | + return stateId => stateId.toLowerCase().indexOf(lowercaseQuery) === 0; | |
230 | + } | |
231 | + | |
232 | + public clearTargetDashboardState(value: string = '') { | |
233 | + this.dashboardStateInput.nativeElement.value = value; | |
234 | + this.actionTypeFormGroup.get('targetDashboardStateId').patchValue(value, {emitEvent: true}); | |
235 | + setTimeout(() => { | |
236 | + this.dashboardStateInput.nativeElement.blur(); | |
237 | + this.dashboardStateInput.nativeElement.focus(); | |
238 | + }, 0); | |
239 | + } | |
240 | + | |
241 | + private validateActionName(): ValidatorFn { | |
242 | + return (c: FormControl) => { | |
243 | + const newName = c.value; | |
244 | + const valid = this.checkActionName(newName, this.widgetActionFormGroup.get('actionSourceId').value); | |
245 | + return !valid ? { | |
246 | + actionNameNotUnique: true | |
247 | + } : null; | |
248 | + }; | |
249 | + } | |
250 | + | |
251 | + private checkActionName(name: string, actionSourceId: string): boolean { | |
252 | + let actionNameIsUnique = true; | |
253 | + if (name && actionSourceId) { | |
254 | + const sourceActions = this.data.actionsData.actionsMap[actionSourceId]; | |
255 | + if (sourceActions) { | |
256 | + const result = sourceActions.filter((sourceAction) => sourceAction.name === name); | |
257 | + if (result && result.length && result[0].id !== this.action.id) { | |
258 | + actionNameIsUnique = false; | |
259 | + } | |
260 | + } | |
261 | + } | |
262 | + return actionNameIsUnique; | |
263 | + } | |
264 | + | |
265 | + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { | |
266 | + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); | |
267 | + const customErrorState = !!(control && control.invalid && this.submitted); | |
268 | + return originalErrorState || customErrorState; | |
269 | + } | |
270 | + | |
271 | + public actionSourceName(actionSource: WidgetActionSource): string { | |
272 | + if (actionSource) { | |
273 | + return this.utils.customTranslation(actionSource.name, actionSource.name); | |
274 | + } else { | |
275 | + return ''; | |
276 | + } | |
277 | + } | |
278 | + | |
279 | + cancel(): void { | |
280 | + this.dialogRef.close(null); | |
281 | + } | |
282 | + | |
283 | + save(): void { | |
284 | + this.submitted = true; | |
285 | + const type: WidgetActionType = this.widgetActionFormGroup.get('type').value; | |
286 | + let result: WidgetActionDescriptorInfo; | |
287 | + if (type === WidgetActionType.customPretty) { | |
288 | + result = {...this.widgetActionFormGroup.value, ...this.actionTypeFormGroup.get('customAction').value}; | |
289 | + } else { | |
290 | + result = {...this.widgetActionFormGroup.value, ...this.actionTypeFormGroup.value}; | |
291 | + } | |
292 | + result.id = this.action.id; | |
293 | + this.dialogRef.close(result); | |
294 | + } | |
295 | +} | ... | ... |
... | ... | @@ -16,7 +16,7 @@ |
16 | 16 | |
17 | 17 | import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes'; |
18 | 18 | import { |
19 | - AfterViewInit, | |
19 | + AfterViewInit, ChangeDetectionStrategy, | |
20 | 20 | Component, |
21 | 21 | ElementRef, |
22 | 22 | forwardRef, |
... | ... | @@ -413,7 +413,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie |
413 | 413 | let fetchObservable: Observable<Array<DataKey>> = null; |
414 | 414 | if (this.datasourceType === DatasourceType.function || this.widgetType === widgetType.alarm) { |
415 | 415 | const dataKeyFilter = this.createDataKeyFilter(this.searchText); |
416 | - const targetKeysList = this.datasourceType === DatasourceType.function ? this.functionTypeKeys : this.alarmKeys; | |
416 | + const targetKeysList = this.widgetType === widgetType.alarm ? this.alarmKeys : this.functionTypeKeys; | |
417 | 417 | fetchObservable = of(targetKeysList.filter(dataKeyFilter)); |
418 | 418 | } else { |
419 | 419 | if (this.entityAliasId) { | ... | ... |
1 | +<!-- | |
2 | + | |
3 | + Copyright © 2016-2019 The Thingsboard Authors | |
4 | + | |
5 | + Licensed under the Apache License, Version 2.0 (the "License"); | |
6 | + you may not use this file except in compliance with the License. | |
7 | + You may obtain a copy of the License at | |
8 | + | |
9 | + http://www.apache.org/licenses/LICENSE-2.0 | |
10 | + | |
11 | + Unless required by applicable law or agreed to in writing, software | |
12 | + distributed under the License is distributed on an "AS IS" BASIS, | |
13 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
14 | + See the License for the specific language governing permissions and | |
15 | + limitations under the License. | |
16 | + | |
17 | +--> | |
18 | +<form [formGroup]="legendConfigForm"> | |
19 | + <fieldset [disabled]="(isLoading$ | async)"> | |
20 | + <div class="mat-content" style="height: 100%;"> | |
21 | + <div class="mat-padding"> | |
22 | + <section fxLayout="column"> | |
23 | + <mat-form-field> | |
24 | + <mat-label translate>legend.direction</mat-label> | |
25 | + <mat-select matInput formControlName="direction" style="min-width: 150px;"> | |
26 | + <mat-option *ngFor="let direction of legendDirections" [value]="direction"> | |
27 | + {{ legendDirectionTranslations.get(direction) | translate }} | |
28 | + </mat-option> | |
29 | + </mat-select> | |
30 | + </mat-form-field> | |
31 | + <mat-form-field> | |
32 | + <mat-label translate>legend.position</mat-label> | |
33 | + <mat-select matInput formControlName="position" style="min-width: 150px;"> | |
34 | + <mat-option *ngFor="let pos of legendPositions" [value]="pos" | |
35 | + [disabled]="legendConfigForm.get('direction').value === legendDirection.row && | |
36 | + (pos === legendPosition.left || pos === legendPosition.right)"> | |
37 | + {{ legendPositionTranslations.get(pos) | translate }} | |
38 | + </mat-option> | |
39 | + </mat-select> | |
40 | + </mat-form-field> | |
41 | + <mat-checkbox fxFlex formControlName="showMin"> | |
42 | + {{ 'legend.show-min' | translate }} | |
43 | + </mat-checkbox> | |
44 | + <mat-checkbox fxFlex formControlName="showMax"> | |
45 | + {{ 'legend.show-max' | translate }} | |
46 | + </mat-checkbox> | |
47 | + <mat-checkbox fxFlex formControlName="showAvg"> | |
48 | + {{ 'legend.show-avg' | translate }} | |
49 | + </mat-checkbox> | |
50 | + <mat-checkbox fxFlex formControlName="showTotal"> | |
51 | + {{ 'legend.show-total' | translate }} | |
52 | + </mat-checkbox> | |
53 | + </section> | |
54 | + </div> | |
55 | + </div> | |
56 | + </fieldset> | |
57 | +</form> | ... | ... |
1 | +/** | |
2 | + * Copyright © 2016-2019 The Thingsboard Authors | |
3 | + * | |
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + * you may not use this file except in compliance with the License. | |
6 | + * You may obtain a copy of the License at | |
7 | + * | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + * | |
10 | + * Unless required by applicable law or agreed to in writing, software | |
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + * See the License for the specific language governing permissions and | |
14 | + * limitations under the License. | |
15 | + */ | |
16 | +:host { | |
17 | + width: 100%; | |
18 | + height: 100%; | |
19 | + form, | |
20 | + fieldset { | |
21 | + height: 100%; | |
22 | + } | |
23 | + | |
24 | + .mat-content { | |
25 | + overflow: hidden; | |
26 | + background-color: #fff; | |
27 | + } | |
28 | + | |
29 | + .mat-padding { | |
30 | + padding: 16px; | |
31 | + } | |
32 | +} | ... | ... |
1 | +/// | |
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | |
3 | +/// | |
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | +/// you may not use this file except in compliance with the License. | |
6 | +/// You may obtain a copy of the License at | |
7 | +/// | |
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | |
9 | +/// | |
10 | +/// Unless required by applicable law or agreed to in writing, software | |
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | |
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | +/// See the License for the specific language governing permissions and | |
14 | +/// limitations under the License. | |
15 | +/// | |
16 | + | |
17 | +import { Component, Inject, InjectionToken, OnInit, ViewContainerRef } from '@angular/core'; | |
18 | +import { Overlay, OverlayRef } from '@angular/cdk/overlay'; | |
19 | +import { PageComponent } from '@shared/components/page.component'; | |
20 | +import { Store } from '@ngrx/store'; | |
21 | +import { AppState } from '@core/core.state'; | |
22 | +import { FormBuilder, FormGroup } from '@angular/forms'; | |
23 | +import { | |
24 | + LegendConfig, | |
25 | + LegendDirection, | |
26 | + legendDirectionTranslationMap, | |
27 | + LegendPosition, | |
28 | + legendPositionTranslationMap | |
29 | +} from '@shared/models/widget.models'; | |
30 | + | |
31 | +export const LEGEND_CONFIG_PANEL_DATA = new InjectionToken<any>('LegendConfigPanelData'); | |
32 | + | |
33 | +export interface LegendConfigPanelData { | |
34 | + legendConfig: LegendConfig; | |
35 | + legendConfigUpdated: (legendConfig: LegendConfig) => void; | |
36 | +} | |
37 | + | |
38 | +@Component({ | |
39 | + selector: 'tb-legend-config-panel', | |
40 | + templateUrl: './legend-config-panel.component.html', | |
41 | + styleUrls: ['./legend-config-panel.component.scss'] | |
42 | +}) | |
43 | +export class LegendConfigPanelComponent extends PageComponent implements OnInit { | |
44 | + | |
45 | + legendConfigForm: FormGroup; | |
46 | + | |
47 | + legendDirection = LegendDirection; | |
48 | + | |
49 | + legendDirections = Object.keys(LegendDirection); | |
50 | + | |
51 | + legendDirectionTranslations = legendDirectionTranslationMap; | |
52 | + | |
53 | + legendPosition = LegendPosition; | |
54 | + | |
55 | + legendPositions = Object.keys(LegendPosition); | |
56 | + | |
57 | + legendPositionTranslations = legendPositionTranslationMap; | |
58 | + | |
59 | + constructor(@Inject(LEGEND_CONFIG_PANEL_DATA) public data: LegendConfigPanelData, | |
60 | + public overlayRef: OverlayRef, | |
61 | + protected store: Store<AppState>, | |
62 | + public fb: FormBuilder, | |
63 | + private overlay: Overlay, | |
64 | + public viewContainerRef: ViewContainerRef) { | |
65 | + super(store); | |
66 | + } | |
67 | + | |
68 | + ngOnInit(): void { | |
69 | + this.legendConfigForm = this.fb.group({ | |
70 | + direction: [this.data.legendConfig.direction, []], | |
71 | + position: [this.data.legendConfig.position, []], | |
72 | + showMin: [this.data.legendConfig.showMin, []], | |
73 | + showMax: [this.data.legendConfig.showMax, []], | |
74 | + showAvg: [this.data.legendConfig.showAvg, []], | |
75 | + showTotal: [this.data.legendConfig.showTotal, []] | |
76 | + }); | |
77 | + this.legendConfigForm.get('direction').valueChanges.subscribe((direction: LegendDirection) => { | |
78 | + this.onDirectionChanged(direction); | |
79 | + }); | |
80 | + this.onDirectionChanged(this.data.legendConfig.direction); | |
81 | + this.legendConfigForm.valueChanges.subscribe(() => { | |
82 | + this.update(); | |
83 | + }); | |
84 | + } | |
85 | + | |
86 | + private onDirectionChanged(direction: LegendDirection) { | |
87 | + if (direction === LegendDirection.row) { | |
88 | + let position: LegendPosition = this.legendConfigForm.get('position').value; | |
89 | + if (position !== LegendPosition.bottom && position !== LegendPosition.top) { | |
90 | + position = LegendPosition.bottom; | |
91 | + } | |
92 | + this.legendConfigForm.patchValue( | |
93 | + { | |
94 | + position, | |
95 | + showMin: false, | |
96 | + showMax: false, | |
97 | + showAvg: false, | |
98 | + showTotal: false | |
99 | + }, {emitEvent: false} | |
100 | + ); | |
101 | + this.legendConfigForm.get('showMin').disable({emitEvent: false}); | |
102 | + this.legendConfigForm.get('showMax').disable({emitEvent: false}); | |
103 | + this.legendConfigForm.get('showAvg').disable({emitEvent: false}); | |
104 | + this.legendConfigForm.get('showTotal').disable({emitEvent: false}); | |
105 | + } else { | |
106 | + this.legendConfigForm.get('showMin').enable({emitEvent: false}); | |
107 | + this.legendConfigForm.get('showMax').enable({emitEvent: false}); | |
108 | + this.legendConfigForm.get('showAvg').enable({emitEvent: false}); | |
109 | + this.legendConfigForm.get('showTotal').enable({emitEvent: false}); | |
110 | + } | |
111 | + } | |
112 | + | |
113 | + update() { | |
114 | + const newLegendConfig: LegendConfig = this.legendConfigForm.value; | |
115 | + this.data.legendConfigUpdated(newLegendConfig); | |
116 | + } | |
117 | + | |
118 | +} | ... | ... |
1 | +<!-- | |
2 | + | |
3 | + Copyright © 2016-2019 The Thingsboard Authors | |
4 | + | |
5 | + Licensed under the Apache License, Version 2.0 (the "License"); | |
6 | + you may not use this file except in compliance with the License. | |
7 | + You may obtain a copy of the License at | |
8 | + | |
9 | + http://www.apache.org/licenses/LICENSE-2.0 | |
10 | + | |
11 | + Unless required by applicable law or agreed to in writing, software | |
12 | + distributed under the License is distributed on an "AS IS" BASIS, | |
13 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
14 | + See the License for the specific language governing permissions and | |
15 | + limitations under the License. | |
16 | + | |
17 | +--> | |
18 | +<button cdkOverlayOrigin #legendConfigPanelOrigin="cdkOverlayOrigin" [disabled]="disabled" | |
19 | + mat-button mat-raised-button color="primary" (click)="openEditMode($event)"> | |
20 | + <mat-icon class="material-icons">toc</mat-icon> | |
21 | + <span translate>legend.settings</span> | |
22 | +</button> | ... | ... |
1 | +/// | |
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | |
3 | +/// | |
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | +/// you may not use this file except in compliance with the License. | |
6 | +/// You may obtain a copy of the License at | |
7 | +/// | |
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | |
9 | +/// | |
10 | +/// Unless required by applicable law or agreed to in writing, software | |
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | |
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | +/// See the License for the specific language governing permissions and | |
14 | +/// limitations under the License. | |
15 | +/// | |
16 | + | |
17 | +import { | |
18 | + Component, | |
19 | + forwardRef, Inject, | |
20 | + Input, | |
21 | + OnDestroy, | |
22 | + OnInit, | |
23 | + ViewChild, | |
24 | + ViewContainerRef | |
25 | +} from '@angular/core'; | |
26 | +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; | |
27 | +import { TranslateService } from '@ngx-translate/core'; | |
28 | +import { MillisecondsToTimeStringPipe } from '@shared/pipe/milliseconds-to-time-string.pipe'; | |
29 | +import { | |
30 | + HistoryWindowType, | |
31 | + Timewindow, | |
32 | + TimewindowType, | |
33 | + initModelFromDefaultTimewindow, cloneSelectedTimewindow | |
34 | +} from '@shared/models/time/time.models'; | |
35 | +import { DatePipe } from '@angular/common'; | |
36 | +import { | |
37 | + Overlay, | |
38 | + CdkOverlayOrigin, | |
39 | + OverlayConfig, | |
40 | + OverlayPositionBuilder, ConnectedPosition, PositionStrategy, OverlayRef | |
41 | +} from '@angular/cdk/overlay'; | |
42 | +import { | |
43 | + TIMEWINDOW_PANEL_DATA, | |
44 | + TimewindowPanelComponent, | |
45 | + TimewindowPanelData | |
46 | +} from '@shared/components/time/timewindow-panel.component'; | |
47 | +import { ComponentPortal, PortalInjector } from '@angular/cdk/portal'; | |
48 | +import { MediaBreakpoints } from '@shared/models/constants'; | |
49 | +import { BreakpointObserver } from '@angular/cdk/layout'; | |
50 | +import { DOCUMENT } from '@angular/common'; | |
51 | +import { WINDOW } from '@core/services/window.service'; | |
52 | +import { TimeService } from '@core/services/time.service'; | |
53 | +import { TooltipPosition } from '@angular/material/typings/tooltip'; | |
54 | +import { deepClone } from '@core/utils'; | |
55 | +import { LegendConfig } from '@shared/models/widget.models'; | |
56 | +import { | |
57 | + LEGEND_CONFIG_PANEL_DATA, | |
58 | + LegendConfigPanelComponent, | |
59 | + LegendConfigPanelData | |
60 | +} from '@home/components/widget/legend-config-panel.component'; | |
61 | + | |
62 | +@Component({ | |
63 | + selector: 'tb-legend-config', | |
64 | + templateUrl: './legend-config.component.html', | |
65 | + styleUrls: [], | |
66 | + providers: [ | |
67 | + { | |
68 | + provide: NG_VALUE_ACCESSOR, | |
69 | + useExisting: forwardRef(() => LegendConfigComponent), | |
70 | + multi: true | |
71 | + } | |
72 | + ] | |
73 | +}) | |
74 | +export class LegendConfigComponent implements OnInit, OnDestroy, ControlValueAccessor { | |
75 | + | |
76 | + @Input() disabled: boolean; | |
77 | + | |
78 | + @ViewChild('legendConfigPanelOrigin', {static: false}) legendConfigPanelOrigin: CdkOverlayOrigin; | |
79 | + | |
80 | + innerValue: LegendConfig; | |
81 | + | |
82 | + private propagateChange = (_: any) => {}; | |
83 | + | |
84 | + constructor(private overlay: Overlay, | |
85 | + public viewContainerRef: ViewContainerRef, | |
86 | + public breakpointObserver: BreakpointObserver, | |
87 | + @Inject(DOCUMENT) private document: Document, | |
88 | + @Inject(WINDOW) private window: Window) { | |
89 | + } | |
90 | + | |
91 | + ngOnInit(): void { | |
92 | + } | |
93 | + | |
94 | + ngOnDestroy(): void { | |
95 | + } | |
96 | + | |
97 | + openEditMode() { | |
98 | + if (this.disabled) { | |
99 | + return; | |
100 | + } | |
101 | + const isGtSm = this.breakpointObserver.isMatched(MediaBreakpoints['gt-sm']); | |
102 | + const position = this.overlay.position(); | |
103 | + const config = new OverlayConfig({ | |
104 | + panelClass: 'tb-legend-config-panel', | |
105 | + backdropClass: 'cdk-overlay-transparent-backdrop', | |
106 | + hasBackdrop: isGtSm, | |
107 | + }); | |
108 | + if (isGtSm) { | |
109 | + config.minWidth = '220px'; | |
110 | + config.maxHeight = '300px'; | |
111 | + const panelHeight = 220; | |
112 | + const panelWidth = 220; | |
113 | + const el = this.legendConfigPanelOrigin.elementRef.nativeElement; | |
114 | + const offset = el.getBoundingClientRect(); | |
115 | + const scrollTop = this.window.pageYOffset || this.document.documentElement.scrollTop || this.document.body.scrollTop || 0; | |
116 | + const scrollLeft = this.window.pageXOffset || this.document.documentElement.scrollLeft || this.document.body.scrollLeft || 0; | |
117 | + const bottomY = offset.bottom - scrollTop; | |
118 | + const leftX = offset.left - scrollLeft; | |
119 | + let originX; | |
120 | + let originY; | |
121 | + let overlayX; | |
122 | + let overlayY; | |
123 | + const wHeight = this.document.documentElement.clientHeight; | |
124 | + const wWidth = this.document.documentElement.clientWidth; | |
125 | + if (bottomY + panelHeight > wHeight) { | |
126 | + originY = 'top'; | |
127 | + overlayY = 'bottom'; | |
128 | + } else { | |
129 | + originY = 'bottom'; | |
130 | + overlayY = 'top'; | |
131 | + } | |
132 | + if (leftX + panelWidth > wWidth) { | |
133 | + originX = 'end'; | |
134 | + overlayX = 'end'; | |
135 | + } else { | |
136 | + originX = 'start'; | |
137 | + overlayX = 'start'; | |
138 | + } | |
139 | + const connectedPosition: ConnectedPosition = { | |
140 | + originX, | |
141 | + originY, | |
142 | + overlayX, | |
143 | + overlayY | |
144 | + }; | |
145 | + config.positionStrategy = position.flexibleConnectedTo(this.legendConfigPanelOrigin.elementRef) | |
146 | + .withPositions([connectedPosition]); | |
147 | + } else { | |
148 | + config.minWidth = '100%'; | |
149 | + config.minHeight = '100%'; | |
150 | + config.positionStrategy = position.global().top('0%').left('0%') | |
151 | + .right('0%').bottom('0%'); | |
152 | + } | |
153 | + | |
154 | + const overlayRef = this.overlay.create(config); | |
155 | + | |
156 | + overlayRef.backdropClick().subscribe(() => { | |
157 | + overlayRef.dispose(); | |
158 | + }); | |
159 | + | |
160 | + const injector = this._createLegendConfigPanelInjector( | |
161 | + overlayRef, | |
162 | + { | |
163 | + legendConfig: deepClone(this.innerValue), | |
164 | + legendConfigUpdated: this.legendConfigUpdated.bind(this) | |
165 | + } | |
166 | + ); | |
167 | + | |
168 | + overlayRef.attach(new ComponentPortal(LegendConfigPanelComponent, this.viewContainerRef, injector)); | |
169 | + } | |
170 | + | |
171 | + private _createLegendConfigPanelInjector(overlayRef: OverlayRef, data: LegendConfigPanelData): PortalInjector { | |
172 | + const injectionTokens = new WeakMap<any, any>([ | |
173 | + [LEGEND_CONFIG_PANEL_DATA, data], | |
174 | + [OverlayRef, overlayRef] | |
175 | + ]); | |
176 | + return new PortalInjector(this.viewContainerRef.injector, injectionTokens); | |
177 | + } | |
178 | + | |
179 | + registerOnChange(fn: any): void { | |
180 | + this.propagateChange = fn; | |
181 | + } | |
182 | + | |
183 | + registerOnTouched(fn: any): void { | |
184 | + } | |
185 | + | |
186 | + setDisabledState(isDisabled: boolean): void { | |
187 | + this.disabled = isDisabled; | |
188 | + } | |
189 | + | |
190 | + writeValue(obj: LegendConfig): void { | |
191 | + this.innerValue = obj; | |
192 | + } | |
193 | + | |
194 | + private legendConfigUpdated(legendConfig: LegendConfig) { | |
195 | + this.innerValue = legendConfig; | |
196 | + this.propagateChange(this.innerValue); | |
197 | + } | |
198 | +} | ... | ... |
... | ... | @@ -181,6 +181,188 @@ |
181 | 181 | </tb-entity-alias-select> |
182 | 182 | </div> |
183 | 183 | </mat-expansion-panel> |
184 | + <mat-expansion-panel class="tb-datasources" *ngIf="widgetType === widgetTypes.alarm && | |
185 | + modelValue?.isDataEnabled" [expanded]="true"> | |
186 | + <mat-expansion-panel-header> | |
187 | + <mat-panel-title> | |
188 | + {{ 'widget-config.alarm-source' | translate }} | |
189 | + </mat-panel-title> | |
190 | + </mat-expansion-panel-header> | |
191 | + <div [formGroup]="alarmSourceSettings" style="padding: 0 5px;"> | |
192 | + <section fxFlex | |
193 | + fxLayout="column" | |
194 | + fxLayoutAlign="center" | |
195 | + fxLayout.gt-sm="row" | |
196 | + fxLayoutAlign.gt-sm="start center"> | |
197 | + <mat-form-field class="tb-datasource-type"> | |
198 | + <mat-select matInput formControlName="type"> | |
199 | + <mat-option *ngFor="let datasourceType of datasourceTypes" [value]="datasourceType"> | |
200 | + {{ datasourceTypesTranslations.get(datasourceType) | translate }} | |
201 | + </mat-option> | |
202 | + </mat-select> | |
203 | + </mat-form-field> | |
204 | + <section class="tb-datasource" [ngSwitch]="alarmSourceSettings.get('type').value"> | |
205 | + <ng-template [ngSwitchCase]="datasourceType.function"> | |
206 | + <mat-form-field floatLabel="always" [fxShow]="widgetType !== widgetTypes.alarm" | |
207 | + class="tb-datasource-name" style="min-width: 200px;"> | |
208 | + <mat-label></mat-label> | |
209 | + <input matInput | |
210 | + placeholder="{{ 'datasource.name' | translate }}" | |
211 | + formControlName="name"> | |
212 | + </mat-form-field> | |
213 | + </ng-template> | |
214 | + <ng-template [ngSwitchCase]="datasourceType.entity"> | |
215 | + <tb-entity-alias-select | |
216 | + [tbRequired]="alarmSourceSettings.get('type').value === datasourceType.entity" | |
217 | + [aliasController]="aliasController" | |
218 | + formControlName="entityAliasId" | |
219 | + [callbacks]="widgetConfigCallbacks"> | |
220 | + </tb-entity-alias-select> | |
221 | + </ng-template> | |
222 | + </section> | |
223 | + <tb-data-keys class="tb-data-keys" fxFlex | |
224 | + [widgetType]="widgetType" | |
225 | + [datasourceType]="alarmSourceSettings.get('type').value" | |
226 | + [aliasController]="aliasController" | |
227 | + [datakeySettingsSchema]="modelValue?.dataKeySettingsSchema" | |
228 | + [callbacks]="widgetConfigCallbacks" | |
229 | + [entityAliasId]="alarmSourceSettings.get('entityAliasId').value" | |
230 | + formControlName="dataKeys"> | |
231 | + </tb-data-keys> | |
232 | + </section> | |
233 | + </div> | |
234 | + </mat-expansion-panel> | |
235 | + </div> | |
236 | + </mat-tab> | |
237 | + <mat-tab label="{{ 'widget-config.settings' | translate }}"> | |
238 | + <div class="mat-content mat-padding" | |
239 | + fxLayout="column" fxLayoutGap="8px"> | |
240 | + <div [formGroup]="widgetSettings" fxLayout="column" fxLayoutGap="8px"> | |
241 | + <span translate>widget-config.general-settings</span> | |
242 | + <div fxLayout="column" fxLayoutAlign="center" fxLayout.gt-sm="row" fxLayoutAlign.gt-sm="start center"> | |
243 | + <mat-form-field fxFlex class="mat-block"> | |
244 | + <mat-label translate>widget-config.title</mat-label> | |
245 | + <input matInput formControlName="title"> | |
246 | + </mat-form-field> | |
247 | + <div fxFlex [fxShow]="widgetSettings.get('showTitle').value"> | |
248 | + <tb-json-object-edit | |
249 | + [editorStyle]="{minHeight: '100px'}" | |
250 | + required | |
251 | + label="{{ 'widget-config.title-style' | translate }}" | |
252 | + formControlName="titleStyle" | |
253 | + ></tb-json-object-edit> | |
254 | + </div> | |
255 | + </div> | |
256 | + <div fxLayout="column" fxLayoutAlign="center" fxLayout.gt-sm="row" fxLayoutAlign.gt-sm="start center" | |
257 | + fxLayoutGap="8px"> | |
258 | + <div fxLayout="row" fxFlex> | |
259 | + <mat-checkbox formControlName="showTitleIcon"> | |
260 | + {{ 'widget-config.display-icon' | translate }} | |
261 | + </mat-checkbox> | |
262 | + </div> | |
263 | + <div fxFlex> | |
264 | + <tb-material-icon-select | |
265 | + formControlName="titleIcon"> | |
266 | + </tb-material-icon-select> | |
267 | + </div> | |
268 | + <tb-color-input fxFlex | |
269 | + label="{{'widget-config.icon-color' | translate}}" | |
270 | + icon="format_color_fill" | |
271 | + openOnInput | |
272 | + formControlName="iconColor"> | |
273 | + </tb-color-input> | |
274 | + <mat-form-field fxFlex> | |
275 | + <mat-label translate>widget-config.icon-size</mat-label> | |
276 | + <input matInput formControlName="iconSize"> | |
277 | + </mat-form-field> | |
278 | + </div> | |
279 | + <div fxLayout="column" fxLayoutAlign="center" fxLayout.gt-sm="row" fxLayoutAlign.gt-sm="start center" | |
280 | + fxLayoutGap="8px"> | |
281 | + <div fxLayout="row"> | |
282 | + <mat-checkbox formControlName="showTitle"> | |
283 | + {{ 'widget-config.display-title' | translate }} | |
284 | + </mat-checkbox> | |
285 | + </div> | |
286 | + <div fxLayout="row"> | |
287 | + <mat-checkbox formControlName="dropShadow"> | |
288 | + {{ 'widget-config.drop-shadow' | translate }} | |
289 | + </mat-checkbox> | |
290 | + </div> | |
291 | + <div fxLayout="row"> | |
292 | + <mat-checkbox formControlName="enableFullscreen"> | |
293 | + {{ 'widget-config.enable-fullscreen' | translate }} | |
294 | + </mat-checkbox> | |
295 | + </div> | |
296 | + <div fxFlex> | |
297 | + <tb-json-object-edit | |
298 | + [editorStyle]="{minHeight: '100px'}" | |
299 | + required | |
300 | + label="{{ 'widget-config.widget-style' | translate }}" | |
301 | + formControlName="widgetStyle" | |
302 | + ></tb-json-object-edit> | |
303 | + </div> | |
304 | + </div> | |
305 | + <div fxLayout="column" fxLayoutAlign="center" fxLayout.gt-sm="row" fxLayoutAlign.gt-sm="start center" | |
306 | + fxLayoutGap="8px"> | |
307 | + <tb-color-input fxFlex | |
308 | + label="{{'widget-config.background-color' | translate}}" | |
309 | + icon="format_color_fill" | |
310 | + openOnInput | |
311 | + formControlName="backgroundColor"> | |
312 | + </tb-color-input> | |
313 | + <tb-color-input fxFlex | |
314 | + label="{{'widget-config.text-color' | translate}}" | |
315 | + icon="format_color_fill" | |
316 | + openOnInput | |
317 | + formControlName="color"> | |
318 | + </tb-color-input> | |
319 | + <mat-form-field fxFlex> | |
320 | + <mat-label translate>widget-config.padding</mat-label> | |
321 | + <input matInput formControlName="padding"> | |
322 | + </mat-form-field> | |
323 | + <mat-form-field fxFlex> | |
324 | + <mat-label translate>widget-config.margin</mat-label> | |
325 | + <input matInput formControlName="margin"> | |
326 | + </mat-form-field> | |
327 | + </div> | |
328 | + <div fxLayout="column" fxLayoutAlign="center" fxLayout.gt-sm="row" fxLayoutAlign.gt-sm="start center" | |
329 | + fxLayoutGap="8px"> | |
330 | + <mat-form-field fxFlex> | |
331 | + <mat-label translate>widget-config.units</mat-label> | |
332 | + <input matInput formControlName="units"> | |
333 | + </mat-form-field> | |
334 | + <mat-form-field fxFlex> | |
335 | + <mat-label translate>widget-config.decimals</mat-label> | |
336 | + <input matInput formControlName="decimals" type="number" min="0" max="15" step="1"> | |
337 | + </mat-form-field> | |
338 | + </div> | |
339 | + <div [fxShow]="widgetType === widgetTypes.timeseries || | |
340 | + widgetType === widgetTypes.latest" | |
341 | + fxLayout="column" fxLayoutAlign="center" fxLayout.gt-sm="row" fxLayoutAlign.gt-sm="start center" | |
342 | + fxLayoutGap="8px"> | |
343 | + <mat-checkbox fxFlex formControlName="showLegend"> | |
344 | + {{ 'widget-config.display-legend' | translate }} | |
345 | + </mat-checkbox> | |
346 | + <section fxFlex fxLayout="row" fxLayoutAlign="start center" style="margin-bottom: 16px;"> | |
347 | + <tb-legend-config formControlName="legendConfig"> | |
348 | + </tb-legend-config> | |
349 | + </section> | |
350 | + </div> | |
351 | + </div> | |
352 | + <div [formGroup]="layoutSettings" fxLayout="column" fxLayoutGap="8px"> | |
353 | + <span translate>widget-config.mobile-mode-settings</span> | |
354 | + <div fxLayout="column" fxLayoutAlign="center" fxLayout.gt-sm="row" fxLayoutAlign.gt-sm="start center" | |
355 | + fxLayoutGap="8px"> | |
356 | + <mat-form-field fxFlex> | |
357 | + <mat-label translate>widget-config.order</mat-label> | |
358 | + <input matInput formControlName="mobileOrder" type="number" step="1"> | |
359 | + </mat-form-field> | |
360 | + <mat-form-field fxFlex> | |
361 | + <mat-label translate>widget-config.height</mat-label> | |
362 | + <input matInput formControlName="mobileHeight" type="number" min="1" max="10" step="1"> | |
363 | + </mat-form-field> | |
364 | + </div> | |
365 | + </div> | |
184 | 366 | </div> |
185 | 367 | </mat-tab> |
186 | 368 | <mat-tab *ngIf="displayAdvanced()" label="{{ 'widget-config.advanced' | translate }}"> |
... | ... | @@ -191,6 +373,10 @@ |
191 | 373 | </tb-json-form> |
192 | 374 | </div> |
193 | 375 | </mat-tab> |
194 | - <mat-tab label="{{ 'widget-config.actions' | translate }}"> | |
376 | + <mat-tab label="{{ 'widget-config.actions' | translate }}" [formGroup]="actionsSettings"> | |
377 | + <tb-manage-widget-actions | |
378 | + [callbacks]="widgetConfigCallbacks" | |
379 | + formControlName="actionsData"> | |
380 | + </tb-manage-widget-actions> | |
195 | 381 | </mat-tab> |
196 | 382 | </mat-tab-group> | ... | ... |
... | ... | @@ -16,5 +16,6 @@ |
16 | 16 | |
17 | 17 | import { EntityAliasSelectCallbacks } from '../alias/entity-alias-select.component.models'; |
18 | 18 | import { DataKeysCallbacks } from './data-keys.component.models'; |
19 | +import { WidgetActionCallbacks } from './action/manage-widget-actions.component.models'; | |
19 | 20 | |
20 | -export type WidgetConfigCallbacks = EntityAliasSelectCallbacks & DataKeysCallbacks; | |
21 | +export type WidgetConfigCallbacks = EntityAliasSelectCallbacks & DataKeysCallbacks & WidgetActionCallbacks; | ... | ... |
... | ... | @@ -21,6 +21,9 @@ |
21 | 21 | .tb-advanced-widget-config { |
22 | 22 | height: 100%; |
23 | 23 | } |
24 | + .tb-advanced-widget-config { | |
25 | + height: 100%; | |
26 | + } | |
24 | 27 | .tb-datasource-type { |
25 | 28 | min-width: 110px; |
26 | 29 | } |
... | ... | @@ -45,6 +48,13 @@ |
45 | 48 | |
46 | 49 | :host ::ng-deep { |
47 | 50 | .tb-widget-config { |
51 | + .mat-tab-body-wrapper { | |
52 | + position: absolute; | |
53 | + top: 49px; | |
54 | + left: 0; | |
55 | + right: 0; | |
56 | + bottom: 0; | |
57 | + } | |
48 | 58 | .mat-tab-body.mat-tab-body-active { |
49 | 59 | .mat-tab-body-content > div { |
50 | 60 | height: 100%; | ... | ... |
... | ... | @@ -14,19 +14,17 @@ |
14 | 14 | /// limitations under the License. |
15 | 15 | /// |
16 | 16 | |
17 | -import { Component, forwardRef, Input, OnInit } from '@angular/core'; | |
17 | +import { ChangeDetectionStrategy, Component, forwardRef, Input, OnInit } from '@angular/core'; | |
18 | 18 | import { PageComponent } from '@shared/components/page.component'; |
19 | 19 | import { Store } from '@ngrx/store'; |
20 | 20 | import { AppState } from '@core/core.state'; |
21 | 21 | import { |
22 | 22 | DataKey, |
23 | 23 | Datasource, |
24 | - DatasourceType, datasourceTypeTranslationMap, | |
25 | - LegendConfig, | |
24 | + DatasourceType, | |
25 | + datasourceTypeTranslationMap, defaultLegendConfig, | |
26 | 26 | WidgetActionDescriptor, |
27 | - WidgetActionSource, | |
28 | - widgetType, | |
29 | - WidgetTypeParameters | |
27 | + widgetType | |
30 | 28 | } from '@shared/models/widget.models'; |
31 | 29 | import { |
32 | 30 | AbstractControl, |
... | ... | @@ -37,7 +35,7 @@ import { |
37 | 35 | FormGroup, |
38 | 36 | NG_VALIDATORS, |
39 | 37 | NG_VALUE_ACCESSOR, |
40 | - Validator, ValidatorFn, | |
38 | + Validator, | |
41 | 39 | Validators |
42 | 40 | } from '@angular/forms'; |
43 | 41 | import { WidgetConfigComponentData } from '@home/models/widget-component.models'; |
... | ... | @@ -55,10 +53,12 @@ import { |
55 | 53 | EntityAliasDialogComponent, |
56 | 54 | EntityAliasDialogData |
57 | 55 | } from '@home/components/alias/entity-alias-dialog.component'; |
58 | -import { tap, mergeMap, map, catchError } from 'rxjs/operators'; | |
56 | +import { catchError, map, mergeMap, tap } from 'rxjs/operators'; | |
59 | 57 | import { MatDialog } from '@angular/material/dialog'; |
60 | 58 | import { EntityService } from '@core/http/entity.service'; |
61 | 59 | import { JsonFormComponentData } from '@shared/components/json-form/json-form-component.models'; |
60 | +import { WidgetActionsData } from './action/manage-widget-actions.component.models'; | |
61 | +import { Dashboard } from '@shared/models/dashboard.models'; | |
62 | 62 | |
63 | 63 | const emptySettingsSchema = { |
64 | 64 | type: 'object', |
... | ... | @@ -106,6 +106,9 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont |
106 | 106 | @Input() |
107 | 107 | functionsOnly: boolean; |
108 | 108 | |
109 | + @Input() | |
110 | + dashboardStates: Array<string>; | |
111 | + | |
109 | 112 | @Input() disabled: boolean; |
110 | 113 | |
111 | 114 | widgetType: widgetType; |
... | ... | @@ -117,34 +120,13 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont |
117 | 120 | widgetConfigCallbacks: WidgetConfigCallbacks = { |
118 | 121 | createEntityAlias: this.createEntityAlias.bind(this), |
119 | 122 | generateDataKey: this.generateDataKey.bind(this), |
120 | - fetchEntityKeys: this.fetchEntityKeys.bind(this) | |
123 | + fetchEntityKeys: this.fetchEntityKeys.bind(this), | |
124 | + fetchDashboardStates: this.fetchDashboardStates.bind(this) | |
121 | 125 | }; |
122 | 126 | |
123 | 127 | widgetEditMode = this.utils.widgetEditMode; |
124 | 128 | |
125 | 129 | selectedTab: number; |
126 | - title: string; | |
127 | - showTitleIcon: boolean; | |
128 | - titleIcon: string; | |
129 | - iconColor: string; | |
130 | - iconSize: string; | |
131 | - showTitle: boolean; | |
132 | - dropShadow: boolean; | |
133 | - enableFullscreen: boolean; | |
134 | - backgroundColor: string; | |
135 | - color: string; | |
136 | - padding: string; | |
137 | - margin: string; | |
138 | - widgetStyle: string; | |
139 | - titleStyle: string; | |
140 | - units: string; | |
141 | - decimals: number; | |
142 | - showLegend: boolean; | |
143 | - legendConfig: LegendConfig; | |
144 | - actions: {[actionSourceId: string]: Array<WidgetActionDescriptor>}; | |
145 | - alarmSource: Datasource; | |
146 | - mobileOrder: number; | |
147 | - mobileHeight: number; | |
148 | 130 | |
149 | 131 | private modelValue: WidgetConfigComponentData; |
150 | 132 | |
... | ... | @@ -152,11 +134,19 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont |
152 | 134 | |
153 | 135 | public dataSettings: FormGroup; |
154 | 136 | public targetDeviceSettings: FormGroup; |
137 | + public alarmSourceSettings: FormGroup; | |
138 | + public widgetSettings: FormGroup; | |
139 | + public layoutSettings: FormGroup; | |
155 | 140 | public advancedSettings: FormGroup; |
141 | + public actionsSettings: FormGroup; | |
156 | 142 | |
157 | 143 | private dataSettingsChangesSubscription: Subscription; |
158 | 144 | private targetDeviceSettingsSubscription: Subscription; |
145 | + private alarmSourceSettingsSubscription: Subscription; | |
146 | + private widgetSettingsSubscription: Subscription; | |
147 | + private layoutSettingsSubscription: Subscription; | |
159 | 148 | private advancedSettingsSubscription: Subscription; |
149 | + private actionsSettingsSubscription: Subscription; | |
160 | 150 | |
161 | 151 | constructor(protected store: Store<AppState>, |
162 | 152 | private utils: UtilsService, |
... | ... | @@ -173,6 +163,47 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont |
173 | 163 | } else { |
174 | 164 | this.datasourceTypes = [DatasourceType.function, DatasourceType.entity]; |
175 | 165 | } |
166 | + this.widgetSettings = this.fb.group({ | |
167 | + title: [null, []], | |
168 | + showTitleIcon: [null, []], | |
169 | + titleIcon: [null, []], | |
170 | + iconColor: [null, []], | |
171 | + iconSize: [null, []], | |
172 | + showTitle: [null, []], | |
173 | + dropShadow: [null, []], | |
174 | + enableFullscreen: [null, []], | |
175 | + backgroundColor: [null, []], | |
176 | + color: [null, []], | |
177 | + padding: [null, []], | |
178 | + margin: [null, []], | |
179 | + widgetStyle: [null, []], | |
180 | + titleStyle: [null, []], | |
181 | + units: [null, []], | |
182 | + decimals: [null, [Validators.min(0), Validators.max(15), Validators.pattern(/^\d*$/)]], | |
183 | + showLegend: [null, []], | |
184 | + legendConfig: [null, []] | |
185 | + }); | |
186 | + this.widgetSettings.get('showTitleIcon').valueChanges.subscribe((value: boolean) => { | |
187 | + if (value) { | |
188 | + this.widgetSettings.get('titleIcon').enable({emitEvent: false}); | |
189 | + } else { | |
190 | + this.widgetSettings.get('titleIcon').disable({emitEvent: false}); | |
191 | + } | |
192 | + }); | |
193 | + this.widgetSettings.get('showLegend').valueChanges.subscribe((value: boolean) => { | |
194 | + if (value) { | |
195 | + this.widgetSettings.get('legendConfig').enable({emitEvent: false}); | |
196 | + } else { | |
197 | + this.widgetSettings.get('legendConfig').disable({emitEvent: false}); | |
198 | + } | |
199 | + }); | |
200 | + this.layoutSettings = this.fb.group({ | |
201 | + mobileOrder: [null, [Validators.pattern(/^-?[0-9]+$/)]], | |
202 | + mobileHeight: [null, [Validators.min(1), Validators.max(10), Validators.pattern(/^\d*$/)]] | |
203 | + }); | |
204 | + this.actionsSettings = this.fb.group({ | |
205 | + actionsData: [null, []] | |
206 | + }); | |
176 | 207 | } |
177 | 208 | |
178 | 209 | private removeChangeSubscriptions() { |
... | ... | @@ -184,10 +215,26 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont |
184 | 215 | this.targetDeviceSettingsSubscription.unsubscribe(); |
185 | 216 | this.targetDeviceSettingsSubscription = null; |
186 | 217 | } |
218 | + if (this.alarmSourceSettingsSubscription) { | |
219 | + this.alarmSourceSettingsSubscription.unsubscribe(); | |
220 | + this.alarmSourceSettingsSubscription = null; | |
221 | + } | |
222 | + if (this.widgetSettingsSubscription) { | |
223 | + this.widgetSettingsSubscription.unsubscribe(); | |
224 | + this.widgetSettingsSubscription = null; | |
225 | + } | |
226 | + if (this.layoutSettingsSubscription) { | |
227 | + this.layoutSettingsSubscription.unsubscribe(); | |
228 | + this.layoutSettingsSubscription = null; | |
229 | + } | |
187 | 230 | if (this.advancedSettingsSubscription) { |
188 | 231 | this.advancedSettingsSubscription.unsubscribe(); |
189 | 232 | this.advancedSettingsSubscription = null; |
190 | 233 | } |
234 | + if (this.actionsSettingsSubscription) { | |
235 | + this.actionsSettingsSubscription.unsubscribe(); | |
236 | + this.actionsSettingsSubscription = null; | |
237 | + } | |
191 | 238 | } |
192 | 239 | |
193 | 240 | private createChangeSubscriptions() { |
... | ... | @@ -197,14 +244,27 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont |
197 | 244 | this.targetDeviceSettingsSubscription = this.targetDeviceSettings.valueChanges.subscribe( |
198 | 245 | () => this.updateTargetDeviceSettings() |
199 | 246 | ); |
247 | + this.alarmSourceSettingsSubscription = this.alarmSourceSettings.valueChanges.subscribe( | |
248 | + () => this.updateAlarmSourceSettings() | |
249 | + ); | |
250 | + this.widgetSettingsSubscription = this.widgetSettings.valueChanges.subscribe( | |
251 | + () => this.updateWidgetSettings() | |
252 | + ); | |
253 | + this.layoutSettingsSubscription = this.layoutSettings.valueChanges.subscribe( | |
254 | + () => this.updateLayoutSettings() | |
255 | + ); | |
200 | 256 | this.advancedSettingsSubscription = this.advancedSettings.valueChanges.subscribe( |
201 | 257 | () => this.updateAdvancedSettings() |
202 | 258 | ); |
259 | + this.actionsSettingsSubscription = this.actionsSettings.valueChanges.subscribe( | |
260 | + () => this.updateActionSettings() | |
261 | + ); | |
203 | 262 | } |
204 | 263 | |
205 | 264 | private buildForms() { |
206 | 265 | this.dataSettings = this.fb.group({}); |
207 | 266 | this.targetDeviceSettings = this.fb.group({}); |
267 | + this.alarmSourceSettings = this.fb.group({}); | |
208 | 268 | this.advancedSettings = this.fb.group({}); |
209 | 269 | if (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.alarm) { |
210 | 270 | this.dataSettings.addControl('useDashboardTimewindow', this.fb.control(null)); |
... | ... | @@ -235,6 +295,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont |
235 | 295 | this.targetDeviceSettings.addControl('targetDeviceAliasId', |
236 | 296 | this.fb.control(null, |
237 | 297 | this.widgetEditMode ? [] : [Validators.required])); |
298 | + } else if (this.widgetType === widgetType.alarm) { | |
299 | + this.alarmSourceSettings = this.buildDatasourceForm(); | |
238 | 300 | } |
239 | 301 | } |
240 | 302 | this.advancedSettings.addControl('settings', |
... | ... | @@ -264,31 +326,54 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont |
264 | 326 | const layout = this.modelValue.layout; |
265 | 327 | if (config) { |
266 | 328 | this.selectedTab = 0; |
267 | - this.title = config.title; | |
268 | - this.showTitleIcon = isDefined(config.showTitleIcon) ? config.showTitleIcon : false; | |
269 | - this.titleIcon = isDefined(config.titleIcon) ? config.titleIcon : ''; | |
270 | - this.iconColor = isDefined(config.iconColor) ? config.iconColor : 'rgba(0, 0, 0, 0.87)'; | |
271 | - this.iconSize = isDefined(config.iconSize) ? config.iconSize : '24px'; | |
272 | - this.showTitle = config.showTitle; | |
273 | - this.dropShadow = isDefined(config.dropShadow) ? config.dropShadow : true; | |
274 | - this.enableFullscreen = isDefined(config.enableFullscreen) ? config.enableFullscreen : true; | |
275 | - this.backgroundColor = config.backgroundColor; | |
276 | - this.color = config.color; | |
277 | - this.padding = config.padding; | |
278 | - this.margin = config.margin; | |
279 | - this.widgetStyle = | |
280 | - JSON.stringify(isDefined(config.widgetStyle) ? config.widgetStyle : {}, null, 2); | |
281 | - this.titleStyle = | |
282 | - JSON.stringify(isDefined(config.titleStyle) ? config.titleStyle : { | |
283 | - fontSize: '16px', | |
284 | - fontWeight: 400 | |
285 | - }, null, 2); | |
286 | - this.units = config.units; | |
287 | - this.decimals = config.decimals; | |
288 | - this.actions = config.actions; | |
289 | - if (!this.actions) { | |
290 | - this.actions = {}; | |
329 | + this.widgetSettings.patchValue({ | |
330 | + title: config.title, | |
331 | + showTitleIcon: isDefined(config.showTitleIcon) ? config.showTitleIcon : false, | |
332 | + titleIcon: isDefined(config.titleIcon) ? config.titleIcon : '', | |
333 | + iconColor: isDefined(config.iconColor) ? config.iconColor : 'rgba(0, 0, 0, 0.87)', | |
334 | + iconSize: isDefined(config.iconSize) ? config.iconSize : '24px', | |
335 | + showTitle: config.showTitle, | |
336 | + dropShadow: isDefined(config.dropShadow) ? config.dropShadow : true, | |
337 | + enableFullscreen: isDefined(config.enableFullscreen) ? config.enableFullscreen : true, | |
338 | + backgroundColor: config.backgroundColor, | |
339 | + color: config.color, | |
340 | + padding: config.padding, | |
341 | + margin: config.margin, | |
342 | + widgetStyle: isDefined(config.widgetStyle) ? config.widgetStyle : {}, | |
343 | + titleStyle: isDefined(config.titleStyle) ? config.titleStyle : { | |
344 | + fontSize: '16px', | |
345 | + fontWeight: 400 | |
346 | + }, | |
347 | + units: config.units, | |
348 | + decimals: config.decimals, | |
349 | + showLegend: isDefined(config.showLegend) ? config.showLegend : | |
350 | + this.widgetType === widgetType.timeseries, | |
351 | + legendConfig: config.legendConfig || defaultLegendConfig(this.widgetType) | |
352 | + }, | |
353 | + {emitEvent: false} | |
354 | + ); | |
355 | + const showTitleIcon: boolean = this.widgetSettings.get('showTitleIcon').value; | |
356 | + if (showTitleIcon) { | |
357 | + this.widgetSettings.get('titleIcon').enable({emitEvent: false}); | |
358 | + } else { | |
359 | + this.widgetSettings.get('titleIcon').disable({emitEvent: false}); | |
360 | + } | |
361 | + const showLegend: boolean = this.widgetSettings.get('showLegend').value; | |
362 | + if (showLegend) { | |
363 | + this.widgetSettings.get('legendConfig').enable({emitEvent: false}); | |
364 | + } else { | |
365 | + this.widgetSettings.get('legendConfig').disable({emitEvent: false}); | |
291 | 366 | } |
367 | + const actionsData: WidgetActionsData = { | |
368 | + actionsMap: config.actions || {}, | |
369 | + actionSources: this.modelValue.actionSources || {} | |
370 | + }; | |
371 | + this.actionsSettings.patchValue( | |
372 | + { | |
373 | + actionsData | |
374 | + }, | |
375 | + {emitEvent: false} | |
376 | + ); | |
292 | 377 | if (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.alarm) { |
293 | 378 | const useDashboardTimewindow = isDefined(config.useDashboardTimewindow) ? |
294 | 379 | config.useDashboardTimewindow : true; |
... | ... | @@ -346,29 +431,42 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont |
346 | 431 | { alarmsPollingInterval: isDefined(config.alarmsPollingInterval) ? |
347 | 432 | config.alarmsPollingInterval : 5}, {emitEvent: false} |
348 | 433 | ); |
349 | - if (config.alarmSource) { | |
350 | - this.alarmSource = config.alarmSource; | |
351 | - } else { | |
352 | - this.alarmSource = null; | |
353 | - } | |
434 | + this.alarmSourceSettings.patchValue( | |
435 | + config.alarmSource, {emitEvent: false} | |
436 | + ); | |
437 | + const alarmSourceType: DatasourceType = this.alarmSourceSettings.get('type').value; | |
438 | + this.alarmSourceSettings.get('entityAliasId').setValidators( | |
439 | + alarmSourceType === DatasourceType.entity ? [Validators.required] : [] | |
440 | + ); | |
441 | + this.alarmSourceSettings.get('entityAliasId').updateValueAndValidity({emitEvent: false}); | |
354 | 442 | } |
355 | 443 | } |
356 | 444 | |
357 | 445 | this.updateSchemaForm(config.settings); |
358 | 446 | |
359 | 447 | if (layout) { |
360 | - this.mobileOrder = layout.mobileOrder; | |
361 | - this.mobileHeight = layout.mobileHeight; | |
448 | + this.layoutSettings.patchValue( | |
449 | + { | |
450 | + mobileOrder: layout.mobileOrder, | |
451 | + mobileHeight: layout.mobileHeight | |
452 | + }, | |
453 | + {emitEvent: false} | |
454 | + ); | |
362 | 455 | } else { |
363 | - this.mobileOrder = undefined; | |
364 | - this.mobileHeight = undefined; | |
456 | + this.layoutSettings.patchValue( | |
457 | + { | |
458 | + mobileOrder: null, | |
459 | + mobileHeight: null | |
460 | + }, | |
461 | + {emitEvent: false} | |
462 | + ); | |
365 | 463 | } |
366 | 464 | } |
367 | 465 | this.createChangeSubscriptions(); |
368 | 466 | } |
369 | 467 | } |
370 | 468 | |
371 | - private buildDatasourceForm(datasource?: Datasource): AbstractControl { | |
469 | + private buildDatasourceForm(datasource?: Datasource): FormGroup { | |
372 | 470 | const dataKeysRequired = !this.modelValue.typeParameters || !this.modelValue.typeParameters.dataKeysOptional; |
373 | 471 | const datasourceFormGroup = this.fb.group( |
374 | 472 | { |
... | ... | @@ -427,6 +525,34 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont |
427 | 525 | } |
428 | 526 | } |
429 | 527 | |
528 | + private updateAlarmSourceSettings() { | |
529 | + if (this.modelValue) { | |
530 | + if (this.modelValue.config) { | |
531 | + const alarmSource: Datasource = this.alarmSourceSettings.value; | |
532 | + this.modelValue.config.alarmSource = alarmSource; | |
533 | + } | |
534 | + this.propagateChange(this.modelValue); | |
535 | + } | |
536 | + } | |
537 | + | |
538 | + private updateWidgetSettings() { | |
539 | + if (this.modelValue) { | |
540 | + if (this.modelValue.config) { | |
541 | + Object.assign(this.modelValue.config, this.widgetSettings.value); | |
542 | + } | |
543 | + this.propagateChange(this.modelValue); | |
544 | + } | |
545 | + } | |
546 | + | |
547 | + private updateLayoutSettings() { | |
548 | + if (this.modelValue) { | |
549 | + if (this.modelValue.layout) { | |
550 | + Object.assign(this.modelValue.layout, this.layoutSettings.value); | |
551 | + } | |
552 | + this.propagateChange(this.modelValue); | |
553 | + } | |
554 | + } | |
555 | + | |
430 | 556 | private updateAdvancedSettings() { |
431 | 557 | if (this.modelValue) { |
432 | 558 | if (this.modelValue.config) { |
... | ... | @@ -437,6 +563,16 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont |
437 | 563 | } |
438 | 564 | } |
439 | 565 | |
566 | + private updateActionSettings() { | |
567 | + if (this.modelValue) { | |
568 | + if (this.modelValue.config) { | |
569 | + const actions = (this.actionsSettings.get('actionsData').value as WidgetActionsData).actionsMap; | |
570 | + this.modelValue.config.actions = actions; | |
571 | + } | |
572 | + this.propagateChange(this.modelValue); | |
573 | + } | |
574 | + } | |
575 | + | |
440 | 576 | public displayAdvanced(): boolean { |
441 | 577 | return this.modelValue.settingsSchema && this.modelValue.settingsSchema.schema; |
442 | 578 | } |
... | ... | @@ -596,6 +732,21 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont |
596 | 732 | ); |
597 | 733 | } |
598 | 734 | |
735 | + private fetchDashboardStates(query: string): Array<string> { | |
736 | + const stateIds = Object.keys(this.dashboardStates); | |
737 | + const result = query ? stateIds.filter(this.createFilterForDashboardState(query)) : stateIds; | |
738 | + if (result && result.length) { | |
739 | + return result; | |
740 | + } else { | |
741 | + return [query]; | |
742 | + } | |
743 | + } | |
744 | + | |
745 | + private createFilterForDashboardState(query: string): (stateId: string) => boolean { | |
746 | + const lowercaseQuery = query.toLowerCase(); | |
747 | + return stateId => stateId.toLowerCase().indexOf(lowercaseQuery) === 0; | |
748 | + } | |
749 | + | |
599 | 750 | public validate(c: FormControl) { |
600 | 751 | if (!this.dataSettings.valid) { |
601 | 752 | return { |
... | ... | @@ -603,6 +754,18 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont |
603 | 754 | valid: false |
604 | 755 | } |
605 | 756 | }; |
757 | + } else if (!this.widgetSettings.valid) { | |
758 | + return { | |
759 | + widgetSettings: { | |
760 | + valid: false | |
761 | + } | |
762 | + }; | |
763 | + } else if (!this.layoutSettings.valid) { | |
764 | + return { | |
765 | + widgetSettings: { | |
766 | + valid: false | |
767 | + } | |
768 | + }; | |
606 | 769 | } else if (!this.advancedSettings.valid) { |
607 | 770 | return { |
608 | 771 | advancedSettings: { |
... | ... | @@ -636,24 +799,6 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont |
636 | 799 | }; |
637 | 800 | } |
638 | 801 | } |
639 | - try { | |
640 | - JSON.parse(this.widgetStyle); | |
641 | - } catch (e) { | |
642 | - return { | |
643 | - widgetStyle: { | |
644 | - valid: false | |
645 | - } | |
646 | - }; | |
647 | - } | |
648 | - try { | |
649 | - JSON.parse(this.titleStyle); | |
650 | - } catch (e) { | |
651 | - return { | |
652 | - titleStyle: { | |
653 | - valid: false | |
654 | - } | |
655 | - }; | |
656 | - } | |
657 | 802 | } |
658 | 803 | return null; |
659 | 804 | } | ... | ... |
... | ... | @@ -38,6 +38,7 @@ import { |
38 | 38 | Datasource, |
39 | 39 | LegendConfig, |
40 | 40 | LegendData, |
41 | + LegendDirection, | |
41 | 42 | LegendPosition, |
42 | 43 | Widget, |
43 | 44 | WidgetActionDescriptor, |
... | ... | @@ -45,7 +46,8 @@ import { |
45 | 46 | WidgetActionType, |
46 | 47 | WidgetResource, |
47 | 48 | widgetType, |
48 | - WidgetTypeParameters | |
49 | + WidgetTypeParameters, | |
50 | + defaultLegendConfig | |
49 | 51 | } from '@shared/models/widget.models'; |
50 | 52 | import { PageComponent } from '@shared/components/page.component'; |
51 | 53 | import { Store } from '@ngrx/store'; |
... | ... | @@ -165,6 +167,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI |
165 | 167 | private ngZone: NgZone, |
166 | 168 | private cd: ChangeDetectorRef) { |
167 | 169 | super(store); |
170 | + this.cssParser.testMode = false; | |
168 | 171 | } |
169 | 172 | |
170 | 173 | ngOnInit(): void { |
... | ... | @@ -179,14 +182,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI |
179 | 182 | this.legendContainerLayoutType = 'column'; |
180 | 183 | |
181 | 184 | if (this.displayLegend) { |
182 | - this.legendConfig = this.widget.config.legendConfig || | |
183 | - { | |
184 | - position: LegendPosition.bottom, | |
185 | - showMin: false, | |
186 | - showMax: false, | |
187 | - showAvg: this.widget.type === widgetType.timeseries, | |
188 | - showTotal: false | |
189 | - }; | |
185 | + this.legendConfig = this.widget.config.legendConfig || defaultLegendConfig(this.widget.type); | |
190 | 186 | this.legendData = { |
191 | 187 | keys: [], |
192 | 188 | data: [] |
... | ... | @@ -194,8 +190,10 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI |
194 | 190 | if (this.legendConfig.position === LegendPosition.top || |
195 | 191 | this.legendConfig.position === LegendPosition.bottom) { |
196 | 192 | this.legendContainerLayoutType = 'column'; |
193 | + this.isLegendFirst = this.legendConfig.position === LegendPosition.top; | |
197 | 194 | } else { |
198 | 195 | this.legendContainerLayoutType = 'row'; |
196 | + this.isLegendFirst = this.legendConfig.position === LegendPosition.left; | |
199 | 197 | } |
200 | 198 | switch (this.legendConfig.position) { |
201 | 199 | case LegendPosition.top: |
... | ... | @@ -352,7 +350,9 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI |
352 | 350 | this.loadFromWidgetInfo(); |
353 | 351 | } |
354 | 352 | ); |
355 | - | |
353 | + setTimeout(() => { | |
354 | + this.dashboardWidget.updateWidgetParams(); | |
355 | + }, 0); | |
356 | 356 | } |
357 | 357 | |
358 | 358 | ngAfterViewInit(): void { |
... | ... | @@ -764,6 +764,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI |
764 | 764 | timeWindowUpdated: (subscription, timeWindowConfig) => { |
765 | 765 | this.ngZone.run(() => { |
766 | 766 | this.widget.config.timewindow = timeWindowConfig; |
767 | + this.cd.detectChanges(); | |
767 | 768 | }); |
768 | 769 | } |
769 | 770 | }; |
... | ... | @@ -924,24 +925,16 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI |
924 | 925 | if (targetDashboardStateId) { |
925 | 926 | stateObject.id = targetDashboardStateId; |
926 | 927 | } |
927 | - const stateParams = { | |
928 | - dashboardId: targetDashboardId, | |
929 | - state: objToBase64([ stateObject ]) | |
930 | - }; | |
931 | 928 | const state = objToBase64([ stateObject ]); |
932 | - const currentUrl = this.route.snapshot.url; | |
929 | + const isSinglePage = this.route.snapshot.data.singlePageMode; | |
933 | 930 | let url; |
934 | - if (currentUrl.length > 1) { | |
935 | - if (currentUrl[currentUrl.length - 2].path === 'dashboard') { | |
936 | - url = `/dashboard/${targetDashboardId}?state=${state}`; | |
937 | - } else { | |
938 | - url = `/dashboards/${targetDashboardId}?state=${state}`; | |
939 | - } | |
940 | - } | |
941 | - if (url) { | |
942 | - const urlTree = this.router.parseUrl(url); | |
943 | - this.router.navigateByUrl(url); | |
931 | + if (isSinglePage) { | |
932 | + url = `/dashboard/${targetDashboardId}?state=${state}`; | |
933 | + } else { | |
934 | + url = `/dashboards/${targetDashboardId}?state=${state}`; | |
944 | 935 | } |
936 | + const urlTree = this.router.parseUrl(url); | |
937 | + this.router.navigateByUrl(url); | |
945 | 938 | break; |
946 | 939 | case WidgetActionType.custom: |
947 | 940 | const customFunction = descriptor.customFunction; | ... | ... |
... | ... | @@ -78,6 +78,7 @@ export interface IDashboardComponent { |
78 | 78 | selectWidget(index: number, delay?: number); |
79 | 79 | getSelectedWidget(): Widget; |
80 | 80 | getEventGridPosition(event: Event): WidgetPosition; |
81 | + notifyGridsterOptionsChanged(); | |
81 | 82 | } |
82 | 83 | |
83 | 84 | declare type DashboardWidgetUpdateOperation = 'add' | 'remove' | 'update'; |
... | ... | @@ -185,7 +186,7 @@ export class DashboardWidgets implements Iterable<DashboardWidget> { |
185 | 186 | |
186 | 187 | highlightWidget(index: number): DashboardWidget { |
187 | 188 | const widget = this.findWidgetAtIndex(index); |
188 | - if (widget && (!this.highlightedMode || !widget.highlighted)) { | |
189 | + if (widget && (!this.highlightedMode || !widget.highlighted || this.highlightedMode && widget.highlighted)) { | |
189 | 190 | this.highlightedMode = true; |
190 | 191 | widget.highlighted = true; |
191 | 192 | this.dashboardWidgets.forEach((dashboardWidget) => { |
... | ... | @@ -248,6 +249,7 @@ export class DashboardWidgets implements Iterable<DashboardWidget> { |
248 | 249 | }); |
249 | 250 | this.sortWidgets(); |
250 | 251 | this.dashboard.gridsterOpts.maxRows = maxRows; |
252 | + this.dashboard.notifyGridsterOptionsChanged(); | |
251 | 253 | } |
252 | 254 | |
253 | 255 | sortWidgets() { | ... | ... |
... | ... | @@ -68,7 +68,6 @@ export interface WidgetContext { |
68 | 68 | width?: number; |
69 | 69 | height?: number; |
70 | 70 | $scope?: IDynamicWidgetComponent; |
71 | - hideTitlePanel?: boolean; | |
72 | 71 | isEdit?: boolean; |
73 | 72 | isMobile?: boolean; |
74 | 73 | dashboard?: IDashboardComponent; |
... | ... | @@ -87,15 +86,18 @@ export interface WidgetContext { |
87 | 86 | stateController?: IStateController; |
88 | 87 | aliasController?: IAliasController; |
89 | 88 | activeEntityInfo?: SubscriptionEntityInfo; |
90 | - widgetTitleTemplate?: string; | |
91 | - widgetTitle?: string; | |
92 | - customHeaderActions?: Array<WidgetHeaderAction>; | |
93 | - widgetActions?: Array<WidgetAction>; | |
94 | 89 | |
95 | 90 | datasources?: Array<Datasource>; |
96 | 91 | data?: Array<DatasourceData>; |
97 | 92 | hiddenData?: Array<{data: DataSet}>; |
98 | 93 | timeWindow?: WidgetTimewindow; |
94 | + | |
95 | + hideTitlePanel?: boolean; | |
96 | + widgetTitleTemplate?: string; | |
97 | + widgetTitle?: string; | |
98 | + customHeaderActions?: Array<WidgetHeaderAction>; | |
99 | + widgetActions?: Array<WidgetAction>; | |
100 | + | |
99 | 101 | } |
100 | 102 | |
101 | 103 | export interface IDynamicWidgetComponent { |
... | ... | @@ -122,7 +124,7 @@ export interface WidgetConfigComponentData { |
122 | 124 | layout: WidgetLayout; |
123 | 125 | widgetType: widgetType; |
124 | 126 | typeParameters: WidgetTypeParameters; |
125 | - actionSources: {[key: string]: WidgetActionSource}; | |
127 | + actionSources: {[actionSourceId: string]: WidgetActionSource}; | |
126 | 128 | isDataEnabled: boolean; |
127 | 129 | settingsSchema: any; |
128 | 130 | dataKeySettingsSchema: any; |
... | ... | @@ -178,7 +180,7 @@ export interface WidgetTypeInstance { |
178 | 180 | getDataKeySettingsSchema?: () => string; |
179 | 181 | typeParameters?: () => WidgetTypeParameters; |
180 | 182 | useCustomDatasources?: () => boolean; |
181 | - actionSources?: () => {[key: string]: WidgetActionSource}; | |
183 | + actionSources?: () => {[actionSourceId: string]: WidgetActionSource}; | |
182 | 184 | |
183 | 185 | onInit?: () => void; |
184 | 186 | onDataUpdated?: () => void; | ... | ... |
... | ... | @@ -14,7 +14,16 @@ |
14 | 14 | /// limitations under the License. |
15 | 15 | /// |
16 | 16 | |
17 | -import { Component, Inject, OnDestroy, OnInit, ViewEncapsulation, ViewChild, NgZone } from '@angular/core'; | |
17 | +import { | |
18 | + Component, | |
19 | + Inject, | |
20 | + OnDestroy, | |
21 | + OnInit, | |
22 | + ViewEncapsulation, | |
23 | + ViewChild, | |
24 | + NgZone, | |
25 | + ChangeDetectorRef, ChangeDetectionStrategy, ApplicationRef | |
26 | +} from '@angular/core'; | |
18 | 27 | import { PageComponent } from '@shared/components/page.component'; |
19 | 28 | import { Store } from '@ngrx/store'; |
20 | 29 | import { AppState } from '@core/core.state'; |
... | ... | @@ -77,7 +86,8 @@ import { EditWidgetComponent } from '@home/pages/dashboard/edit-widget.component |
77 | 86 | selector: 'tb-dashboard-page', |
78 | 87 | templateUrl: './dashboard-page.component.html', |
79 | 88 | styleUrls: ['./dashboard-page.component.scss'], |
80 | - encapsulation: ViewEncapsulation.None | |
89 | + encapsulation: ViewEncapsulation.None, | |
90 | + // changeDetection: ChangeDetectionStrategy.OnPush | |
81 | 91 | }) |
82 | 92 | export class DashboardPageComponent extends PageComponent implements IDashboardController, OnDestroy { |
83 | 93 | |
... | ... | @@ -149,7 +159,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC |
149 | 159 | dashboardTimewindow: null, |
150 | 160 | state: null, |
151 | 161 | stateController: null, |
152 | - aliasController: null | |
162 | + aliasController: null, | |
163 | + runChangeDetection: this.runChangeDetection.bind(this) | |
153 | 164 | }; |
154 | 165 | |
155 | 166 | addWidgetFabButtons: FooterFabButtons = { |
... | ... | @@ -204,12 +215,15 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC |
204 | 215 | private dashboardService: DashboardService, |
205 | 216 | private itembuffer: ItemBufferService, |
206 | 217 | private fb: FormBuilder, |
207 | - private dialog: MatDialog) { | |
218 | + private dialog: MatDialog, | |
219 | + private ngZone: NgZone, | |
220 | + private cd: ChangeDetectorRef) { | |
208 | 221 | super(store); |
209 | 222 | |
210 | 223 | this.rxSubscriptions.push(this.route.data.subscribe( |
211 | 224 | (data) => { |
212 | 225 | this.init(data); |
226 | + this.runChangeDetection(); | |
213 | 227 | } |
214 | 228 | )); |
215 | 229 | |
... | ... | @@ -294,6 +308,12 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC |
294 | 308 | this.rxSubscriptions.length = 0; |
295 | 309 | } |
296 | 310 | |
311 | + public runChangeDetection() { | |
312 | + /*setTimeout(() => { | |
313 | + this.cd.detectChanges(); | |
314 | + });*/ | |
315 | + } | |
316 | + | |
297 | 317 | public openToolbar() { |
298 | 318 | this.isToolbarOpenedAnimate = true; |
299 | 319 | this.isToolbarOpened = true; |
... | ... | @@ -646,7 +666,9 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC |
646 | 666 | this.editingWidgetLayoutOriginal = widgetLayout; |
647 | 667 | this.editingLayoutCtx.widgets[index] = widget; |
648 | 668 | this.editingLayoutCtx.widgetLayouts[widget.id] = widgetLayout; |
649 | - this.editingLayoutCtx.ctrl.highlightWidget(index, 0); | |
669 | + setTimeout(() => { | |
670 | + this.editingLayoutCtx.ctrl.highlightWidget(index, 0); | |
671 | + }, 0); | |
650 | 672 | } |
651 | 673 | |
652 | 674 | onEditWidgetClosed() { | ... | ... |
... | ... | @@ -25,6 +25,7 @@ import { |
25 | 25 | WidgetPosition |
26 | 26 | } from '@home/models/dashboard-component.models'; |
27 | 27 | import { Observable } from 'rxjs'; |
28 | +import { ChangeDetectorRef } from '@angular/core'; | |
28 | 29 | |
29 | 30 | export declare type DashboardPageScope = 'tenant' | 'customer'; |
30 | 31 | |
... | ... | @@ -34,6 +35,7 @@ export interface DashboardContext { |
34 | 35 | dashboardTimewindow: Timewindow; |
35 | 36 | aliasController: IAliasController; |
36 | 37 | stateController: IStateController; |
38 | + runChangeDetection: () => void; | |
37 | 39 | } |
38 | 40 | |
39 | 41 | export interface IDashboardController { | ... | ... |
... | ... | @@ -21,6 +21,7 @@ |
21 | 21 | [aliasController]="aliasController" |
22 | 22 | [functionsOnly]="widgetEditMode" |
23 | 23 | [entityAliases]="dashboard.configuration.entityAliases" |
24 | + [dashboardStates]="dashboard.configuration.states" | |
24 | 25 | formControlName="widgetConfig"> |
25 | 26 | </tb-widget-config> |
26 | 27 | </fieldset> | ... | ... |
... | ... | @@ -14,7 +14,7 @@ |
14 | 14 | /// limitations under the License. |
15 | 15 | /// |
16 | 16 | |
17 | -import { Component, OnInit, Input, OnChanges, SimpleChanges, ViewChild } from '@angular/core'; | |
17 | +import { Component, OnInit, Input, OnChanges, SimpleChanges, ViewChild, ChangeDetectionStrategy } from '@angular/core'; | |
18 | 18 | import { PageComponent } from '@shared/components/page.component'; |
19 | 19 | import { Store } from '@ngrx/store'; |
20 | 20 | import { AppState } from '@core/core.state'; | ... | ... |
... | ... | @@ -85,8 +85,8 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo |
85 | 85 | this.rxSubscriptions.push(this.dashboard.dashboardTimewindowChanged.subscribe( |
86 | 86 | (dashboardTimewindow) => { |
87 | 87 | this.dashboardCtx.dashboardTimewindow = dashboardTimewindow; |
88 | - } | |
89 | - ) | |
88 | + this.dashboardCtx.runChangeDetection(); | |
89 | + }) | |
90 | 90 | ); |
91 | 91 | this.initHotKeys(); |
92 | 92 | } | ... | ... |
... | ... | @@ -90,7 +90,7 @@ export class DefaultStateControllerComponent extends StateControllerComponent im |
90 | 90 | |
91 | 91 | protected onStateChanged() { |
92 | 92 | this.stateObject = this.parseState(this.currentState); |
93 | - this.gotoState(this.stateObject[0].id, true); | |
93 | + this.gotoState(this.stateObject[0].id, false); | |
94 | 94 | } |
95 | 95 | |
96 | 96 | protected stateControllerId(): string { | ... | ... |
... | ... | @@ -95,7 +95,7 @@ export class EntityStateControllerComponent extends StateControllerComponent imp |
95 | 95 | protected onStateChanged() { |
96 | 96 | this.stateObject = this.parseState(this.currentState); |
97 | 97 | this.selectedStateIndex = this.stateObject.length - 1; |
98 | - this.gotoState(this.stateObject[this.stateObject.length - 1].id, true); | |
98 | + this.gotoState(this.stateObject[this.stateObject.length - 1].id, false); | |
99 | 99 | } |
100 | 100 | |
101 | 101 | protected stateControllerId(): string { | ... | ... |
... | ... | @@ -120,7 +120,7 @@ export class DashboardAutocompleteComponent implements ControlValueAccessor, OnI |
120 | 120 | } |
121 | 121 | |
122 | 122 | ngAfterViewInit(): void { |
123 | - this.selectFirstDashboardIfNeeded(); | |
123 | + // this.selectFirstDashboardIfNeeded(); | |
124 | 124 | } |
125 | 125 | |
126 | 126 | selectFirstDashboardIfNeeded(): void { |
... | ... | @@ -159,6 +159,7 @@ export class DashboardAutocompleteComponent implements ControlValueAccessor, OnI |
159 | 159 | } else { |
160 | 160 | this.modelValue = null; |
161 | 161 | this.selectDashboardFormGroup.get('dashboard').patchValue(null, {emitEvent: true}); |
162 | + this.selectFirstDashboardIfNeeded(); | |
162 | 163 | } |
163 | 164 | } |
164 | 165 | ... | ... |
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 class="tb-material-icons-dialog" style="min-width: 600px;"> | |
19 | + <mat-toolbar fxLayout="row" color="primary"> | |
20 | + <h2>{{ 'icon.select-icon' | translate }}</h2> | |
21 | + <span fxFlex></span> | |
22 | + <section fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> | |
23 | + <mat-slide-toggle [formControl]="showAllControl"> | |
24 | + </mat-slide-toggle> | |
25 | + <label translate>icon.show-all</label> | |
26 | + </section> | |
27 | + <button mat-button mat-icon-button | |
28 | + (click)="cancel()" | |
29 | + type="button"> | |
30 | + <mat-icon class="material-icons">close</mat-icon> | |
31 | + </button> | |
32 | + </mat-toolbar> | |
33 | + <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> | |
34 | + </mat-progress-bar> | |
35 | + <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div> | |
36 | + <div class="tb-absolute-fill tb-icons-load" *ngIf="loadingIcons$ | async" fxLayout="column" fxLayoutAlign="center center"> | |
37 | + <mat-spinner color="accent" mode="indeterminate" diameter="40"></mat-spinner> | |
38 | + </div> | |
39 | + <div mat-dialog-content> | |
40 | + <div class="mat-content mat-padding" fxLayout="column"> | |
41 | + <fieldset [disabled]="(isLoading$ | async)"> | |
42 | + <ng-template ngFor let-icon [ngForOf]="icons$ | async" let-last="last"> | |
43 | + <ng-container #iconButtons> | |
44 | + <button *ngIf="icon === selectedIcon" | |
45 | + class="tb-select-icon-button" | |
46 | + mat-button mat-icon-button | |
47 | + mat-raised-button | |
48 | + color="primary" | |
49 | + (click)="selectIcon(icon)" | |
50 | + matTooltip="{{ icon }}" | |
51 | + matTooltipPosition="above" | |
52 | + type="button"> | |
53 | + <mat-icon>{{icon}}</mat-icon> | |
54 | + </button> | |
55 | + <button *ngIf="icon !== selectedIcon" | |
56 | + class="tb-select-icon-button" | |
57 | + mat-button mat-icon-button | |
58 | + (click)="selectIcon(icon)" | |
59 | + matTooltip="{{ icon }}" | |
60 | + matTooltipPosition="above" | |
61 | + type="button"> | |
62 | + <mat-icon>{{icon}}</mat-icon> | |
63 | + </button> | |
64 | + </ng-container> | |
65 | + </ng-template> | |
66 | + </fieldset> | |
67 | + </div> | |
68 | + </div> | |
69 | + <div mat-dialog-actions fxLayout="row"> | |
70 | + <span fxFlex></span> | |
71 | + <button mat-button | |
72 | + type="button" | |
73 | + [disabled]="(isLoading$ | async)" | |
74 | + (click)="cancel()"> | |
75 | + {{ 'action.cancel' | translate }} | |
76 | + </button> | |
77 | + </div> | |
78 | +</form> | ... | ... |
1 | +/** | |
2 | + * Copyright © 2016-2019 The Thingsboard Authors | |
3 | + * | |
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + * you may not use this file except in compliance with the License. | |
6 | + * You may obtain a copy of the License at | |
7 | + * | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + * | |
10 | + * Unless required by applicable law or agreed to in writing, software | |
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + * See the License for the specific language governing permissions and | |
14 | + * limitations under the License. | |
15 | + */ | |
16 | +:host { | |
17 | + .tb-material-icons-dialog { | |
18 | + position: relative; | |
19 | + } | |
20 | + .tb-icons-load { | |
21 | + top: 64px; | |
22 | + z-index: 3; | |
23 | + background: rgba(255, 255, 255, .75); | |
24 | + } | |
25 | +} | |
26 | + | |
27 | +:host ::ng-deep { | |
28 | + .tb-material-icons-dialog { | |
29 | + button.mat-icon-button.tb-select-icon-button { | |
30 | + width: 56px; | |
31 | + height: 56px; | |
32 | + padding: 16px; | |
33 | + margin: 10px; | |
34 | + border: solid 1px #ffa500; | |
35 | + border-radius: 0%; | |
36 | + line-height: 0; | |
37 | + } | |
38 | + } | |
39 | +} | ... | ... |
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, QueryList, ViewChildren, TemplateRef, AfterViewInit } from '@angular/core'; | |
18 | +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material'; | |
19 | +import { Store } from '@ngrx/store'; | |
20 | +import { AppState } from '@core/core.state'; | |
21 | +import { Router } from '@angular/router'; | |
22 | +import { DialogComponent } from '@app/shared/components/dialog.component'; | |
23 | +import { UtilsService } from '@core/services/utils.service'; | |
24 | +import { FormControl } from '@angular/forms'; | |
25 | +import { Observable, of, merge, noop } from 'rxjs'; | |
26 | +import { delay, map, mergeMap, share, startWith, tap, mapTo } from 'rxjs/operators'; | |
27 | +import { DashboardInfo } from '@shared/models/dashboard.models'; | |
28 | +import { MatTab } from '@angular/material/tabs'; | |
29 | + | |
30 | +export interface MaterialIconsDialogData { | |
31 | + icon: string; | |
32 | +} | |
33 | + | |
34 | +@Component({ | |
35 | + selector: 'tb-material-icons-dialog', | |
36 | + templateUrl: './material-icons-dialog.component.html', | |
37 | + providers: [], | |
38 | + styleUrls: ['./material-icons-dialog.component.scss'] | |
39 | +}) | |
40 | +export class MaterialIconsDialogComponent extends DialogComponent<MaterialIconsDialogComponent, string> | |
41 | + implements OnInit, AfterViewInit { | |
42 | + | |
43 | + @ViewChildren('iconButtons') iconButtons: QueryList<HTMLElement>; | |
44 | + | |
45 | + selectedIcon: string; | |
46 | + icons$: Observable<Array<string>>; | |
47 | + loadingIcons$: Observable<boolean>; | |
48 | + | |
49 | + showAllControl: FormControl; | |
50 | + | |
51 | + constructor(protected store: Store<AppState>, | |
52 | + protected router: Router, | |
53 | + @Inject(MAT_DIALOG_DATA) public data: MaterialIconsDialogData, | |
54 | + private utils: UtilsService, | |
55 | + public dialogRef: MatDialogRef<MaterialIconsDialogComponent, string>) { | |
56 | + super(store, router, dialogRef); | |
57 | + this.selectedIcon = data.icon; | |
58 | + this.showAllControl = new FormControl(false); | |
59 | + } | |
60 | + | |
61 | + ngOnInit(): void { | |
62 | + this.icons$ = this.showAllControl.valueChanges.pipe( | |
63 | + map((showAll) => { | |
64 | + return {firstTime: false, showAll}; | |
65 | + }), | |
66 | + startWith<{firstTime: boolean, showAll: boolean}>({firstTime: true, showAll: false}), | |
67 | + mergeMap((data) => { | |
68 | + if (data.showAll) { | |
69 | + return this.utils.getMaterialIcons().pipe(delay(100)); | |
70 | + } else { | |
71 | + const res = of(this.utils.getCommonMaterialIcons()); | |
72 | + return data.firstTime ? res : res.pipe(delay(50)); | |
73 | + } | |
74 | + }), | |
75 | + share() | |
76 | + ); | |
77 | + } | |
78 | + | |
79 | + ngAfterViewInit(): void { | |
80 | + this.loadingIcons$ = merge( | |
81 | + this.showAllControl.valueChanges.pipe( | |
82 | + mapTo(true), | |
83 | + ), | |
84 | + this.iconButtons.changes.pipe( | |
85 | + delay(100), | |
86 | + mapTo( false), | |
87 | + ) | |
88 | + ).pipe( | |
89 | + tap((loadingIcons) => { | |
90 | + if (loadingIcons) { | |
91 | + this.showAllControl.disable({emitEvent: false}); | |
92 | + } else { | |
93 | + this.showAllControl.enable({emitEvent: false}); | |
94 | + } | |
95 | + }), | |
96 | + share() | |
97 | + ); | |
98 | + } | |
99 | + | |
100 | + selectIcon(icon: string) { | |
101 | + this.dialogRef.close(icon); | |
102 | + } | |
103 | + | |
104 | + cancel(): void { | |
105 | + this.dialogRef.close(null); | |
106 | + } | |
107 | + | |
108 | +} | ... | ... |
... | ... | @@ -14,7 +14,7 @@ |
14 | 14 | /// limitations under the License. |
15 | 15 | /// |
16 | 16 | |
17 | -import { Component, Input, HostListener } from '@angular/core'; | |
17 | +import { Component, HostListener, Input } from '@angular/core'; | |
18 | 18 | import { PageComponent } from '@shared/components/page.component'; |
19 | 19 | import { Store } from '@ngrx/store'; |
20 | 20 | import { AppState } from '@core/core.state'; | ... | ... |
... | ... | @@ -18,7 +18,7 @@ import { |
18 | 18 | Directive, |
19 | 19 | ElementRef, |
20 | 20 | EventEmitter, |
21 | - Input, OnChanges, | |
21 | + Input, OnChanges, OnDestroy, | |
22 | 22 | Output, SimpleChanges, |
23 | 23 | ViewContainerRef |
24 | 24 | } from '@angular/core'; |
... | ... | @@ -29,7 +29,7 @@ import { TbAnchorComponent } from '@shared/components/tb-anchor.component'; |
29 | 29 | @Directive({ |
30 | 30 | selector: '[tb-fullscreen]' |
31 | 31 | }) |
32 | -export class FullscreenDirective implements OnChanges { | |
32 | +export class FullscreenDirective implements OnChanges, OnDestroy { | |
33 | 33 | |
34 | 34 | fullscreenValue = false; |
35 | 35 | |
... | ... | @@ -69,6 +69,12 @@ export class FullscreenDirective implements OnChanges { |
69 | 69 | } |
70 | 70 | } |
71 | 71 | |
72 | + ngOnDestroy(): void { | |
73 | + if (this.fullscreen) { | |
74 | + this.exitFullscreen(); | |
75 | + } | |
76 | + } | |
77 | + | |
72 | 78 | enterFullscreen() { |
73 | 79 | const targetElement: HTMLElement = this.fullscreenElement || this.elementRef.nativeElement; |
74 | 80 | this.parentElement = targetElement.parentElement; | ... | ... |
... | ... | @@ -17,7 +17,7 @@ |
17 | 17 | --> |
18 | 18 | <div class="tb-js-func" style="background: #fff;" [ngClass]="{'tb-disabled': disabled, 'fill-height': fillHeight}" |
19 | 19 | tb-fullscreen |
20 | - [fullscreen]="fullscreen" (fullscreenChanged)="onFullscreen()" fxLayout="column"> | |
20 | + [fullscreen]="fullscreen" fxLayout="column"> | |
21 | 21 | <div fxLayout="row" fxLayoutAlign="start center" style="height: 40px;" class="tb-js-func-toolbar"> |
22 | 22 | <label class="tb-title no-padding">{{'function ' + (functionName ? functionName : '') + '(' + functionArgsString + ') {'}}</label> |
23 | 23 | <span fxFlex></span> |
... | ... | @@ -25,6 +25,7 @@ |
25 | 25 | {{'js-func.tidy' | translate }} |
26 | 26 | </button> |
27 | 27 | <button type='button' mat-button mat-icon-button (click)="fullscreen = !fullscreen" |
28 | + class="tb-mat-32" | |
28 | 29 | matTooltip="{{(fullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}" |
29 | 30 | matTooltipPosition="above"> |
30 | 31 | <mat-icon class="material-icons">{{ fullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon> | ... | ... |
... | ... | @@ -16,11 +16,11 @@ |
16 | 16 | .tb-js-func { |
17 | 17 | position: relative; |
18 | 18 | |
19 | - .tb-disabled { | |
19 | + &.tb-disabled { | |
20 | 20 | color: rgba(0, 0, 0, .38); |
21 | 21 | } |
22 | 22 | |
23 | - .fill-height { | |
23 | + &.fill-height { | |
24 | 24 | height: 100%; |
25 | 25 | } |
26 | 26 | |
... | ... | @@ -41,15 +41,20 @@ |
41 | 41 | } |
42 | 42 | |
43 | 43 | .tb-js-func-toolbar { |
44 | - .mat-button.tidy { | |
44 | + button.mat-button, button.mat-icon-button, button.mat-icon-button.tb-mat-32 { | |
45 | + align-items: center; | |
46 | + vertical-align: middle; | |
45 | 47 | min-width: 32px; |
46 | 48 | min-height: 15px; |
47 | 49 | padding: 4px; |
48 | - margin: 0 5px 0 0; | |
50 | + margin: 0; | |
49 | 51 | font-size: .8rem; |
50 | 52 | line-height: 15px; |
51 | 53 | color: #7b7b7b; |
52 | 54 | background: rgba(220, 220, 220, .35); |
55 | + &:not(:last-child) { | |
56 | + margin-right: 4px; | |
57 | + } | |
53 | 58 | } |
54 | 59 | } |
55 | 60 | } | ... | ... |
... | ... | @@ -14,7 +14,16 @@ |
14 | 14 | /// limitations under the License. |
15 | 15 | /// |
16 | 16 | |
17 | -import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild, ViewEncapsulation } from '@angular/core'; | |
17 | +import { | |
18 | + ChangeDetectionStrategy, | |
19 | + Component, | |
20 | + ElementRef, | |
21 | + forwardRef, | |
22 | + Input, OnDestroy, | |
23 | + OnInit, | |
24 | + ViewChild, | |
25 | + ViewEncapsulation | |
26 | +} from '@angular/core'; | |
18 | 27 | import { ControlValueAccessor, FormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, Validator } from '@angular/forms'; |
19 | 28 | import * as ace from 'ace-builds'; |
20 | 29 | import { coerceBooleanProperty } from '@angular/cdk/coercion'; |
... | ... | @@ -24,6 +33,7 @@ import { AppState } from '@core/core.state'; |
24 | 33 | import { UtilsService } from '@core/services/utils.service'; |
25 | 34 | import { isUndefined } from '@app/core/utils'; |
26 | 35 | import { TranslateService } from '@ngx-translate/core'; |
36 | +import { CancelAnimationFrame, RafService } from '@core/services/raf.service'; | |
27 | 37 | |
28 | 38 | @Component({ |
29 | 39 | selector: 'tb-js-func', |
... | ... | @@ -43,12 +53,14 @@ import { TranslateService } from '@ngx-translate/core'; |
43 | 53 | ], |
44 | 54 | encapsulation: ViewEncapsulation.None |
45 | 55 | }) |
46 | -export class JsFuncComponent implements OnInit, ControlValueAccessor, Validator { | |
56 | +export class JsFuncComponent implements OnInit, OnDestroy, ControlValueAccessor, Validator { | |
47 | 57 | |
48 | 58 | @ViewChild('javascriptEditor', {static: true}) |
49 | 59 | javascriptEditorElmRef: ElementRef; |
50 | 60 | |
51 | 61 | private jsEditor: ace.Ace.Editor; |
62 | + private editorsResizeCaf: CancelAnimationFrame; | |
63 | + private editorResizeListener: any; | |
52 | 64 | |
53 | 65 | @Input() functionName: string; |
54 | 66 | |
... | ... | @@ -100,7 +112,8 @@ export class JsFuncComponent implements OnInit, ControlValueAccessor, Validator |
100 | 112 | constructor(public elementRef: ElementRef, |
101 | 113 | private utils: UtilsService, |
102 | 114 | private translate: TranslateService, |
103 | - protected store: Store<AppState>) { | |
115 | + protected store: Store<AppState>, | |
116 | + private raf: RafService) { | |
104 | 117 | } |
105 | 118 | |
106 | 119 | ngOnInit(): void { |
... | ... | @@ -137,6 +150,28 @@ export class JsFuncComponent implements OnInit, ControlValueAccessor, Validator |
137 | 150 | this.cleanupJsErrors(); |
138 | 151 | this.updateView(); |
139 | 152 | }); |
153 | + this.editorResizeListener = this.onAceEditorResize.bind(this); | |
154 | + // @ts-ignore | |
155 | + addResizeListener(editorElement, this.editorResizeListener); | |
156 | + } | |
157 | + | |
158 | + ngOnDestroy(): void { | |
159 | + if (this.editorResizeListener) { | |
160 | + const editorElement = this.javascriptEditorElmRef.nativeElement; | |
161 | + // @ts-ignore | |
162 | + removeResizeListener(editorElement, this.editorResizeListener); | |
163 | + } | |
164 | + } | |
165 | + | |
166 | + private onAceEditorResize() { | |
167 | + if (this.editorsResizeCaf) { | |
168 | + this.editorsResizeCaf(); | |
169 | + this.editorsResizeCaf = null; | |
170 | + } | |
171 | + this.editorsResizeCaf = this.raf.raf(() => { | |
172 | + this.jsEditor.resize(); | |
173 | + this.jsEditor.renderer.updateFull(); | |
174 | + }); | |
140 | 175 | } |
141 | 176 | |
142 | 177 | registerOnChange(fn: any): void { |
... | ... | @@ -298,12 +333,4 @@ export class JsFuncComponent implements OnInit, ControlValueAccessor, Validator |
298 | 333 | } |
299 | 334 | } |
300 | 335 | |
301 | - onFullscreen() { | |
302 | - if (this.jsEditor) { | |
303 | - setTimeout(() => { | |
304 | - this.jsEditor.resize(); | |
305 | - }, 0); | |
306 | - } | |
307 | - } | |
308 | - | |
309 | 336 | } | ... | ... |
... | ... | @@ -32,6 +32,6 @@ |
32 | 32 | </div> |
33 | 33 | <div fxFlex="0%" id="tb-json-panel" tb-toast toastTarget="jsonObjectEditor" |
34 | 34 | class="tb-json-object-panel" fxLayout="column"> |
35 | - <div fxFlex #jsonEditor id="tb-json-input" [ngClass]="{'fill-height': fillHeight}"></div> | |
35 | + <div #jsonEditor id="tb-json-input" [ngStyle]="editorStyle" [ngClass]="{'fill-height': fillHeight}"></div> | |
36 | 36 | </div> |
37 | 37 | </div> | ... | ... |
... | ... | @@ -15,7 +15,7 @@ |
15 | 15 | /// |
16 | 16 | |
17 | 17 | import { |
18 | - Attribute, | |
18 | + Attribute, ChangeDetectionStrategy, | |
19 | 19 | Component, |
20 | 20 | ElementRef, |
21 | 21 | forwardRef, |
... | ... | @@ -60,6 +60,8 @@ export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Va |
60 | 60 | |
61 | 61 | @Input() fillHeight: boolean; |
62 | 62 | |
63 | + @Input() editorStyle: {[klass: string]: any}; | |
64 | + | |
63 | 65 | private requiredValue: boolean; |
64 | 66 | get required(): boolean { |
65 | 67 | return this.requiredValue; | ... | ... |
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 fxLayout="row" [formGroup]="materialIconFormGroup"> | |
19 | + <mat-icon (click)="openIconDialog()">{{materialIconFormGroup.get('icon').value}}</mat-icon> | |
20 | + <mat-form-field> | |
21 | + <mat-label translate>icon.icon</mat-label> | |
22 | + <input matInput formControlName="icon" (mousedown)="openIconDialog()"> | |
23 | + </mat-form-field> | |
24 | +</div> | ... | ... |
1 | +/** | |
2 | + * Copyright © 2016-2019 The Thingsboard Authors | |
3 | + * | |
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + * you may not use this file except in compliance with the License. | |
6 | + * You may obtain a copy of the License at | |
7 | + * | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + * | |
10 | + * Unless required by applicable law or agreed to in writing, software | |
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + * See the License for the specific language governing permissions and | |
14 | + * limitations under the License. | |
15 | + */ | |
16 | +:host { | |
17 | + .mat-icon { | |
18 | + padding: 4px; | |
19 | + margin: 8px 4px 4px; | |
20 | + cursor: pointer; | |
21 | + border: solid 1px rgba(0, 0, 0, .27); | |
22 | + } | |
23 | +} | ... | ... |
1 | +/// | |
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | |
3 | +/// | |
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | +/// you may not use this file except in compliance with the License. | |
6 | +/// You may obtain a copy of the License at | |
7 | +/// | |
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | |
9 | +/// | |
10 | +/// Unless required by applicable law or agreed to in writing, software | |
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | |
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | +/// See the License for the specific language governing permissions and | |
14 | +/// limitations under the License. | |
15 | +/// | |
16 | + | |
17 | +import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core'; | |
18 | +import { PageComponent } from '@shared/components/page.component'; | |
19 | +import { Store } from '@ngrx/store'; | |
20 | +import { AppState } from '@core/core.state'; | |
21 | +import { DataKey, DatasourceType } from '@shared/models/widget.models'; | |
22 | +import { | |
23 | + ControlValueAccessor, | |
24 | + FormBuilder, | |
25 | + FormControl, | |
26 | + FormGroup, | |
27 | + NG_VALIDATORS, | |
28 | + NG_VALUE_ACCESSOR, | |
29 | + Validator, | |
30 | + Validators | |
31 | +} from '@angular/forms'; | |
32 | +import { UtilsService } from '@core/services/utils.service'; | |
33 | +import { TranslateService } from '@ngx-translate/core'; | |
34 | +import { MatDialog } from '@angular/material/dialog'; | |
35 | +import { EntityService } from '@core/http/entity.service'; | |
36 | +import { DataKeysCallbacks } from '@home/components/widget/data-keys.component.models'; | |
37 | +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; | |
38 | +import { Observable, of } from 'rxjs'; | |
39 | +import { map, mergeMap, tap } from 'rxjs/operators'; | |
40 | +import { alarmFields } from '@shared/models/alarm.models'; | |
41 | +import { coerceBooleanProperty } from '@angular/cdk/coercion'; | |
42 | +import { DialogService } from '@core/services/dialog.service'; | |
43 | + | |
44 | +@Component({ | |
45 | + selector: 'tb-material-icon-select', | |
46 | + templateUrl: './material-icon-select.component.html', | |
47 | + styleUrls: ['./material-icon-select.component.scss'], | |
48 | + providers: [ | |
49 | + { | |
50 | + provide: NG_VALUE_ACCESSOR, | |
51 | + useExisting: forwardRef(() => MaterialIconSelectComponent), | |
52 | + multi: true | |
53 | + } | |
54 | + ] | |
55 | +}) | |
56 | +export class MaterialIconSelectComponent extends PageComponent implements OnInit, ControlValueAccessor { | |
57 | + | |
58 | + @Input() | |
59 | + disabled: boolean; | |
60 | + | |
61 | + private modelValue: string; | |
62 | + | |
63 | + private propagateChange = null; | |
64 | + | |
65 | + public materialIconFormGroup: FormGroup; | |
66 | + | |
67 | + constructor(protected store: Store<AppState>, | |
68 | + private dialogs: DialogService, | |
69 | + private fb: FormBuilder) { | |
70 | + super(store); | |
71 | + } | |
72 | + | |
73 | + ngOnInit(): void { | |
74 | + this.materialIconFormGroup = this.fb.group({ | |
75 | + icon: [null, []] | |
76 | + }); | |
77 | + | |
78 | + this.materialIconFormGroup.valueChanges.subscribe(() => { | |
79 | + this.updateModel(); | |
80 | + }); | |
81 | + } | |
82 | + | |
83 | + registerOnChange(fn: any): void { | |
84 | + this.propagateChange = fn; | |
85 | + } | |
86 | + | |
87 | + registerOnTouched(fn: any): void { | |
88 | + } | |
89 | + | |
90 | + setDisabledState(isDisabled: boolean): void { | |
91 | + this.disabled = isDisabled; | |
92 | + if (isDisabled) { | |
93 | + this.materialIconFormGroup.disable({emitEvent: false}); | |
94 | + } else { | |
95 | + this.materialIconFormGroup.enable({emitEvent: false}); | |
96 | + } | |
97 | + } | |
98 | + | |
99 | + writeValue(value: string): void { | |
100 | + this.modelValue = value; | |
101 | + this.materialIconFormGroup.patchValue( | |
102 | + { icon: this.modelValue }, {emitEvent: false} | |
103 | + ); | |
104 | + } | |
105 | + | |
106 | + private updateModel() { | |
107 | + const icon: string = this.materialIconFormGroup.get('icon').value; | |
108 | + if (this.modelValue !== icon) { | |
109 | + this.modelValue = icon; | |
110 | + this.propagateChange(this.modelValue); | |
111 | + } | |
112 | + } | |
113 | + | |
114 | + openIconDialog() { | |
115 | + this.dialogs.materialIconPicker(this.materialIconFormGroup.get('icon').value).subscribe( | |
116 | + (icon) => { | |
117 | + if (icon) { | |
118 | + this.materialIconFormGroup.patchValue( | |
119 | + {icon}, {emitEvent: true} | |
120 | + ); | |
121 | + } | |
122 | + } | |
123 | + ); | |
124 | + } | |
125 | +} | ... | ... |
... | ... | @@ -18,7 +18,7 @@ |
18 | 18 | <button *ngIf="asButton" cdkOverlayOrigin #timewindowPanelOrigin="cdkOverlayOrigin" [disabled]="disabled" |
19 | 19 | mat-raised-button color="primary" (click)="openEditMode($event)"> |
20 | 20 | <mat-icon class="material-icons">query_builder</mat-icon> |
21 | - <span>{{innerValue.displayValue}}</span> | |
21 | + <span>{{innerValue?.displayValue}}</span> | |
22 | 22 | </button> |
23 | 23 | <section *ngIf="!asButton" cdkOverlayOrigin #timewindowPanelOrigin="cdkOverlayOrigin" |
24 | 24 | class="tb-timewindow" fxLayout="row" fxLayoutAlign="start center"> |
... | ... | @@ -32,7 +32,7 @@ |
32 | 32 | (click)="openEditMode($event)" |
33 | 33 | matTooltip="{{ 'timewindow.edit' | translate }}" |
34 | 34 | [matTooltipPosition]="tooltipPosition"> |
35 | - {{innerValue.displayValue}} | |
35 | + {{innerValue?.displayValue}} | |
36 | 36 | </span> |
37 | 37 | <button *ngIf="direction === 'right'" [disabled]="disabled" mat-button mat-icon-button class="tb-mat-32" |
38 | 38 | (click)="openEditMode($event)" | ... | ... |
... | ... | @@ -121,7 +121,7 @@ export interface WidgetActionSource { |
121 | 121 | multiple: boolean; |
122 | 122 | } |
123 | 123 | |
124 | -export const widgetActionSources: {[key: string]: WidgetActionSource} = { | |
124 | +export const widgetActionSources: {[acionSourceId: string]: WidgetActionSource} = { | |
125 | 125 | headerButton: |
126 | 126 | { |
127 | 127 | name: 'widget-action.header-button', |
... | ... | @@ -156,7 +156,7 @@ export interface WidgetControllerDescriptor { |
156 | 156 | settingsSchema?: string | any; |
157 | 157 | dataKeySettingsSchema?: string | any; |
158 | 158 | typeParameters?: WidgetTypeParameters; |
159 | - actionSources?: {[key: string]: WidgetActionSource}; | |
159 | + actionSources?: {[actionSourceId: string]: WidgetActionSource}; | |
160 | 160 | } |
161 | 161 | |
162 | 162 | export interface WidgetType extends BaseData<WidgetTypeId> { |
... | ... | @@ -204,6 +204,17 @@ export interface LegendConfig { |
204 | 204 | showTotal: boolean; |
205 | 205 | } |
206 | 206 | |
207 | +export function defaultLegendConfig(wType: widgetType): LegendConfig { | |
208 | + return { | |
209 | + direction: LegendDirection.column, | |
210 | + position: LegendPosition.bottom, | |
211 | + showMin: false, | |
212 | + showMax: false, | |
213 | + showAvg: wType === widgetType.timeseries, | |
214 | + showTotal: false | |
215 | + }; | |
216 | +} | |
217 | + | |
207 | 218 | export interface KeyInfo { |
208 | 219 | name: string; |
209 | 220 | label?: string; |
... | ... | @@ -301,7 +312,15 @@ export const widgetActionTypeTranslationMap = new Map<WidgetActionType, string>( |
301 | 312 | ] |
302 | 313 | ); |
303 | 314 | |
304 | -export interface WidgetActionDescriptor { | |
315 | +export interface CustomActionDescriptor { | |
316 | + customFunction?: string; | |
317 | + customResources?: Array<WidgetResource>; | |
318 | + customHtml?: string; | |
319 | + customCss?: string; | |
320 | +} | |
321 | + | |
322 | +export interface WidgetActionDescriptor extends CustomActionDescriptor { | |
323 | + id: string; | |
305 | 324 | name: string; |
306 | 325 | icon: string; |
307 | 326 | displayName?: string; |
... | ... | @@ -311,10 +330,6 @@ export interface WidgetActionDescriptor { |
311 | 330 | openRightLayout?: boolean; |
312 | 331 | setEntityId?: boolean; |
313 | 332 | stateEntityParamName?: string; |
314 | - customFunction?: string; | |
315 | - customResources?: Array<WidgetResource>; | |
316 | - customHtml?: string; | |
317 | - customCss?: string; | |
318 | 333 | } |
319 | 334 | |
320 | 335 | export interface WidgetConfig { | ... | ... |
... | ... | @@ -106,6 +106,8 @@ import { MatChipDraggableDirective } from './components/mat-chip-draggable.direc |
106 | 106 | import { ColorInputComponent } from './components/color-input.component'; |
107 | 107 | import { JsFuncComponent } from './components/js-func.component'; |
108 | 108 | import { JsonFormComponent } from './components/json-form/json-form.component'; |
109 | +import { MaterialIconsDialogComponent } from '@shared/components/dialog/material-icons-dialog.component'; | |
110 | +import { MaterialIconSelectComponent } from '@shared/components/material-icon-select.component'; | |
109 | 111 | |
110 | 112 | @NgModule({ |
111 | 113 | providers: [ |
... | ... | @@ -121,7 +123,8 @@ import { JsonFormComponent } from './components/json-form/json-form.component'; |
121 | 123 | TimewindowPanelComponent, |
122 | 124 | DashboardSelectPanelComponent, |
123 | 125 | MatSpinner, |
124 | - ColorPickerDialogComponent | |
126 | + ColorPickerDialogComponent, | |
127 | + MaterialIconsDialogComponent | |
125 | 128 | ], |
126 | 129 | declarations: [ |
127 | 130 | FooterComponent, |
... | ... | @@ -166,7 +169,9 @@ import { JsonFormComponent } from './components/json-form/json-form.component'; |
166 | 169 | FabToolbarComponent, |
167 | 170 | WidgetsBundleSelectComponent, |
168 | 171 | ColorPickerDialogComponent, |
172 | + MaterialIconsDialogComponent, | |
169 | 173 | ColorInputComponent, |
174 | + MaterialIconSelectComponent, | |
170 | 175 | JsonFormComponent, |
171 | 176 | NospacePipe, |
172 | 177 | MillisecondsToTimeStringPipe, |
... | ... | @@ -299,7 +304,9 @@ import { JsonFormComponent } from './components/json-form/json-form.component'; |
299 | 304 | HotkeyModule, |
300 | 305 | ColorPickerModule, |
301 | 306 | ColorPickerDialogComponent, |
307 | + MaterialIconsDialogComponent, | |
302 | 308 | ColorInputComponent, |
309 | + MaterialIconSelectComponent, | |
303 | 310 | JsonFormComponent, |
304 | 311 | NospacePipe, |
305 | 312 | MillisecondsToTimeStringPipe, | ... | ... |
... | ... | @@ -1641,6 +1641,7 @@ |
1641 | 1641 | "action": "Action", |
1642 | 1642 | "add-action": "Add action", |
1643 | 1643 | "search-actions": "Search actions", |
1644 | + "no-actions-text": "No actions found", | |
1644 | 1645 | "action-source": "Action source", |
1645 | 1646 | "action-source-required": "Action source is required.", |
1646 | 1647 | "action-name": "Name", | ... | ... |
... | ... | @@ -286,7 +286,7 @@ pre.tb-highlight { |
286 | 286 | font-size: 16px !important; |
287 | 287 | } |
288 | 288 | |
289 | -.tb-timewindow-panel { | |
289 | +.tb-timewindow-panel, .tb-legend-config-panel { | |
290 | 290 | overflow: hidden; |
291 | 291 | background: #fff; |
292 | 292 | border-radius: 4px; |
... | ... | @@ -764,6 +764,13 @@ mat-label { |
764 | 764 | right: 0; |
765 | 765 | } |
766 | 766 | |
767 | + .tb-layout-fill { | |
768 | + margin: 0; | |
769 | + width: 100%; | |
770 | + min-height: 100%; | |
771 | + height: 100%; | |
772 | + } | |
773 | + | |
767 | 774 | .tb-progress-cover { |
768 | 775 | position: absolute; |
769 | 776 | top: 0; | ... | ... |
ui-ngx/src/typings/rawloader.typings.d.ts
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 | + | |
17 | +declare module '!raw-loader!*' { | |
18 | + const contents: string; | |
19 | + export = contents; | |
20 | +} | ... | ... |