Commit b4f230dbc8dca59d43a831ab22d20d5a562475aa
1 parent
e264f7b8
Improve markdown component. Add new helps.
Showing
39 changed files
with
1724 additions
and
247 deletions
@@ -73,7 +73,7 @@ | @@ -73,7 +73,7 @@ | ||
73 | "ngx-drag-drop": "^2.0.0", | 73 | "ngx-drag-drop": "^2.0.0", |
74 | "ngx-flowchart": "git://github.com/thingsboard/ngx-flowchart.git#master", | 74 | "ngx-flowchart": "git://github.com/thingsboard/ngx-flowchart.git#master", |
75 | "ngx-hm-carousel": "^2.0.0-rc.1", | 75 | "ngx-hm-carousel": "^2.0.0-rc.1", |
76 | - "ngx-markdown": "^10.1.1", | 76 | + "ngx-markdown": "^11.1.3", |
77 | "ngx-sharebuttons": "^8.0.5", | 77 | "ngx-sharebuttons": "^8.0.5", |
78 | "ngx-translate-messageformat-compiler": "^4.9.0", | 78 | "ngx-translate-messageformat-compiler": "^4.9.0", |
79 | "objectpath": "^2.0.0", | 79 | "objectpath": "^2.0.0", |
@@ -55,11 +55,12 @@ export class DynamicComponentFactoryService { | @@ -55,11 +55,12 @@ export class DynamicComponentFactoryService { | ||
55 | public createDynamicComponentFactory<T>( | 55 | public createDynamicComponentFactory<T>( |
56 | componentType: Type<T>, | 56 | componentType: Type<T>, |
57 | template: string, | 57 | template: string, |
58 | - modules?: Type<any>[]): Observable<ComponentFactory<T>> { | 58 | + modules?: Type<any>[], |
59 | + preserveWhitespaces?: boolean): Observable<ComponentFactory<T>> { | ||
59 | const dymamicComponentFactorySubject = new ReplaySubject<ComponentFactory<T>>(); | 60 | const dymamicComponentFactorySubject = new ReplaySubject<ComponentFactory<T>>(); |
60 | import('@angular/compiler').then( | 61 | import('@angular/compiler').then( |
61 | () => { | 62 | () => { |
62 | - const comp = this.createDynamicComponent(componentType, template); | 63 | + const comp = this.createDynamicComponent(componentType, template, preserveWhitespaces); |
63 | let moduleImports: Type<any>[] = [CommonModule]; | 64 | let moduleImports: Type<any>[] = [CommonModule]; |
64 | if (modules) { | 65 | if (modules) { |
65 | moduleImports = [...moduleImports, ...modules]; | 66 | moduleImports = [...moduleImports, ...modules]; |
@@ -103,10 +104,11 @@ export class DynamicComponentFactoryService { | @@ -103,10 +104,11 @@ export class DynamicComponentFactoryService { | ||
103 | } | 104 | } |
104 | } | 105 | } |
105 | 106 | ||
106 | - private createDynamicComponent<T>(componentType: Type<T>, template: string): Type<T> { | 107 | + private createDynamicComponent<T>(componentType: Type<T>, template: string, preserveWhitespaces?: boolean): Type<T> { |
107 | // noinspection AngularMissingOrInvalidDeclarationInModule | 108 | // noinspection AngularMissingOrInvalidDeclarationInModule |
108 | return Component({ | 109 | return Component({ |
109 | - template | 110 | + template, |
111 | + preserveWhitespaces | ||
110 | })(componentType); | 112 | })(componentType); |
111 | } | 113 | } |
112 | 114 |
@@ -18,7 +18,8 @@ import { Injectable } from '@angular/core'; | @@ -18,7 +18,8 @@ import { Injectable } from '@angular/core'; | ||
18 | import { HttpClient } from '@angular/common/http'; | 18 | import { HttpClient } from '@angular/common/http'; |
19 | import { TranslateService } from '@ngx-translate/core'; | 19 | import { TranslateService } from '@ngx-translate/core'; |
20 | import { Observable, of } from 'rxjs'; | 20 | import { Observable, of } from 'rxjs'; |
21 | -import { catchError, tap } from 'rxjs/operators'; | 21 | +import { catchError, mergeMap, tap } from 'rxjs/operators'; |
22 | +import { helpBaseUrl } from '@shared/models/constants'; | ||
22 | 23 | ||
23 | const NOT_FOUND_CONTENT = '## Not found'; | 24 | const NOT_FOUND_CONTENT = '## Not found'; |
24 | 25 | ||
@@ -27,6 +28,8 @@ const NOT_FOUND_CONTENT = '## Not found'; | @@ -27,6 +28,8 @@ const NOT_FOUND_CONTENT = '## Not found'; | ||
27 | }) | 28 | }) |
28 | export class HelpService { | 29 | export class HelpService { |
29 | 30 | ||
31 | + private helpBaseUrl = helpBaseUrl; | ||
32 | + | ||
30 | private helpCache: {[lang: string]: {[key: string]: string}} = {}; | 33 | private helpCache: {[lang: string]: {[key: string]: string}} = {}; |
31 | 34 | ||
32 | constructor( | 35 | constructor( |
@@ -52,6 +55,9 @@ export class HelpService { | @@ -52,6 +55,9 @@ export class HelpService { | ||
52 | return of(NOT_FOUND_CONTENT); | 55 | return of(NOT_FOUND_CONTENT); |
53 | } | 56 | } |
54 | }), | 57 | }), |
58 | + mergeMap((content) => { | ||
59 | + return this.processIncludes(this.processVariables(content)); | ||
60 | + }), | ||
55 | tap((content) => { | 61 | tap((content) => { |
56 | let langContent = this.helpCache[lang]; | 62 | let langContent = this.helpCache[lang]; |
57 | if (!langContent) { | 63 | if (!langContent) { |
@@ -68,4 +74,25 @@ export class HelpService { | @@ -68,4 +74,25 @@ export class HelpService { | ||
68 | return this.http.get(`/assets/help/${lang}/${key}.md`, {responseType: 'text'} ); | 74 | return this.http.get(`/assets/help/${lang}/${key}.md`, {responseType: 'text'} ); |
69 | } | 75 | } |
70 | 76 | ||
77 | + private processVariables(content: string): string { | ||
78 | + const baseUrlReg = /\${baseUrl}/g; | ||
79 | + return content.replace(baseUrlReg, this.helpBaseUrl); | ||
80 | + } | ||
81 | + | ||
82 | + private processIncludes(content: string): Observable<string> { | ||
83 | + const includesRule = /{% include (.*) %}/; | ||
84 | + const match = includesRule.exec(content); | ||
85 | + if (match) { | ||
86 | + const key = match[1]; | ||
87 | + return this.getHelpContent(key).pipe( | ||
88 | + mergeMap((include) => { | ||
89 | + content = content.replace(match[0], include); | ||
90 | + return this.processIncludes(content); | ||
91 | + }) | ||
92 | + ); | ||
93 | + } else { | ||
94 | + return of(content); | ||
95 | + } | ||
96 | + } | ||
97 | + | ||
71 | } | 98 | } |
@@ -46,7 +46,8 @@ | @@ -46,7 +46,8 @@ | ||
46 | [functionArgs]="['$event', 'widgetContext', 'entityId', 'entityName', 'htmlTemplate', 'additionalParams', 'entityLabel']" | 46 | [functionArgs]="['$event', 'widgetContext', 'entityId', 'entityName', 'htmlTemplate', 'additionalParams', 'entityLabel']" |
47 | [disableUndefinedCheck]="true" | 47 | [disableUndefinedCheck]="true" |
48 | [validationArgs]="[]" | 48 | [validationArgs]="[]" |
49 | - [editorCompleter]="customPrettyActionEditorCompleter"> | 49 | + [editorCompleter]="customPrettyActionEditorCompleter" |
50 | + helpId="widget/action/custom_pretty_action_fn"> | ||
50 | </tb-js-func> | 51 | </tb-js-func> |
51 | </div> | 52 | </div> |
52 | </div> | 53 | </div> |
@@ -96,7 +96,8 @@ | @@ -96,7 +96,8 @@ | ||
96 | [functionArgs]="['$event', 'widgetContext', 'entityId', 'entityName', 'htmlTemplate', 'additionalParams', 'entityLabel']" | 96 | [functionArgs]="['$event', 'widgetContext', 'entityId', 'entityName', 'htmlTemplate', 'additionalParams', 'entityLabel']" |
97 | [disableUndefinedCheck]="true" | 97 | [disableUndefinedCheck]="true" |
98 | [validationArgs]="[]" | 98 | [validationArgs]="[]" |
99 | - [editorCompleter]="customPrettyActionEditorCompleter"> | 99 | + [editorCompleter]="customPrettyActionEditorCompleter" |
100 | + helpId="widget/action/custom_pretty_action_fn"> | ||
100 | </tb-js-func> | 101 | </tb-js-func> |
101 | </mat-tab> | 102 | </mat-tab> |
102 | </mat-tab-group> | 103 | </mat-tab-group> |
@@ -224,6 +224,7 @@ | @@ -224,6 +224,7 @@ | ||
224 | [globalVariables]="functionScopeVariables" | 224 | [globalVariables]="functionScopeVariables" |
225 | [validationArgs]="[]" | 225 | [validationArgs]="[]" |
226 | [editorCompleter]="customActionEditorCompleter" | 226 | [editorCompleter]="customActionEditorCompleter" |
227 | + helpId="widget/action/custom_action_fn" | ||
227 | ></tb-js-func> | 228 | ></tb-js-func> |
228 | </ng-template> | 229 | </ng-template> |
229 | <ng-template [ngSwitchCase]="widgetActionType.customPretty"> | 230 | <ng-template [ngSwitchCase]="widgetActionType.customPretty"> |
@@ -70,6 +70,7 @@ | @@ -70,6 +70,7 @@ | ||
70 | [globalVariables]="functionScopeVariables" | 70 | [globalVariables]="functionScopeVariables" |
71 | [validationArgs]="[[1, 1],[1, '1']]" | 71 | [validationArgs]="[[1, 1],[1, '1']]" |
72 | resultType="any" | 72 | resultType="any" |
73 | + helpId="widget/config/datakey_generation_fn" | ||
73 | formControlName="funcBody"> | 74 | formControlName="funcBody"> |
74 | </tb-js-func> | 75 | </tb-js-func> |
75 | </section> | 76 | </section> |
@@ -82,6 +83,7 @@ | @@ -82,6 +83,7 @@ | ||
82 | [globalVariables]="functionScopeVariables" | 83 | [globalVariables]="functionScopeVariables" |
83 | [validationArgs]="[[1, 1, 1, 1, 1],[1, '1', '1', 1, '1']]" | 84 | [validationArgs]="[[1, 1, 1, 1, 1],[1, '1', '1', 1, '1']]" |
84 | resultType="any" | 85 | resultType="any" |
86 | + helpId="widget/config/datakey_postprocess_fn" | ||
85 | formControlName="postFuncBody"> | 87 | formControlName="postFuncBody"> |
86 | </tb-js-func> | 88 | </tb-js-func> |
87 | <label *ngIf="dataKeyFormGroup.get('usePostProcessing').value" class="tb-title" style="margin-left: 15px;"> | 89 | <label *ngIf="dataKeyFormGroup.get('usePostProcessing').value" class="tb-title" style="margin-left: 15px;"> |
@@ -15,4 +15,4 @@ | @@ -15,4 +15,4 @@ | ||
15 | limitations under the License. | 15 | limitations under the License. |
16 | 16 | ||
17 | --> | 17 | --> |
18 | -<markdown [data]="markdownText" lineNumbers class="tb-markdown-view" (click)="markdownClick($event)"></markdown> | 18 | +<tb-markdown [data]="markdownText" lineNumbers (click)="markdownClick($event)"></tb-markdown> |
@@ -107,9 +107,9 @@ import { ComponentType } from '@angular/cdk/portal'; | @@ -107,9 +107,9 @@ import { ComponentType } from '@angular/cdk/portal'; | ||
107 | import { EMBED_DASHBOARD_DIALOG_TOKEN } from '@home/components/widget/dialog/embed-dashboard-dialog-token'; | 107 | import { EMBED_DASHBOARD_DIALOG_TOKEN } from '@home/components/widget/dialog/embed-dashboard-dialog-token'; |
108 | import { MobileService } from '@core/services/mobile.service'; | 108 | import { MobileService } from '@core/services/mobile.service'; |
109 | import { DialogService } from '@core/services/dialog.service'; | 109 | import { DialogService } from '@core/services/dialog.service'; |
110 | -import { TbPopoverService } from '@shared/components/popover.component'; | ||
111 | import { DashboardPageComponent } from '@home/components/dashboard-page/dashboard-page.component'; | 110 | import { DashboardPageComponent } from '@home/components/dashboard-page/dashboard-page.component'; |
112 | import { PopoverPlacement } from '@shared/components/popover.models'; | 111 | import { PopoverPlacement } from '@shared/components/popover.models'; |
112 | +import { TbPopoverService } from '@shared/components/popover.service'; | ||
113 | 113 | ||
114 | @Component({ | 114 | @Component({ |
115 | selector: 'tb-widget', | 115 | selector: 'tb-widget', |
@@ -16,5 +16,5 @@ | @@ -16,5 +16,5 @@ | ||
16 | 16 | ||
17 | --> | 17 | --> |
18 | <ng-container *ngIf="markdownText$ | async as text;"> | 18 | <ng-container *ngIf="markdownText$ | async as text;"> |
19 | - <markdown [data]="text" lineNumbers (ready)="onMarkdownReady()" class="tb-help-markdown tb-markdown-view" (click)="markdownClick($event)"></markdown> | 19 | + <tb-markdown [style]="style" [data]="text" lineNumbers (ready)="onMarkdownReady()" markdownClass="tb-help-markdown" (click)="markdownClick($event)"></tb-markdown> |
20 | </ng-container> | 20 | </ng-container> |
@@ -13,16 +13,14 @@ | @@ -13,16 +13,14 @@ | ||
13 | * See the License for the specific language governing permissions and | 13 | * See the License for the specific language governing permissions and |
14 | * limitations under the License. | 14 | * limitations under the License. |
15 | */ | 15 | */ |
16 | -:host { | 16 | + |
17 | +:host ::ng-deep { | ||
17 | .tb-help-markdown { | 18 | .tb-help-markdown { |
18 | overflow: auto; | 19 | overflow: auto; |
19 | max-width: 80vw; | 20 | max-width: 80vw; |
20 | max-height: 80vh; | 21 | max-height: 80vh; |
21 | margin-top: 30px; | 22 | margin-top: 30px; |
22 | } | 23 | } |
23 | -} | ||
24 | - | ||
25 | -:host ::ng-deep { | ||
26 | .tb-help-markdown.tb-markdown-view { | 24 | .tb-help-markdown.tb-markdown-view { |
27 | h1, h2, h3, h4, h5, h6 { | 25 | h1, h2, h3, h4, h5, h6 { |
28 | &:first-child { | 26 | &:first-child { |
@@ -22,7 +22,7 @@ import { | @@ -22,7 +22,7 @@ import { | ||
22 | Output, SimpleChanges | 22 | Output, SimpleChanges |
23 | } from '@angular/core'; | 23 | } from '@angular/core'; |
24 | import { BehaviorSubject } from 'rxjs'; | 24 | import { BehaviorSubject } from 'rxjs'; |
25 | -import { delay, share } from 'rxjs/operators'; | 25 | +import { share } from 'rxjs/operators'; |
26 | import { HelpService } from '@core/services/help.service'; | 26 | import { HelpService } from '@core/services/help.service'; |
27 | 27 | ||
28 | @Component({ | 28 | @Component({ |
@@ -34,8 +34,12 @@ export class HelpMarkdownComponent implements OnDestroy, OnInit, OnChanges { | @@ -34,8 +34,12 @@ export class HelpMarkdownComponent implements OnDestroy, OnInit, OnChanges { | ||
34 | 34 | ||
35 | @Input() helpId: string; | 35 | @Input() helpId: string; |
36 | 36 | ||
37 | + @Input() helpContent: string; | ||
38 | + | ||
37 | @Input() visible: boolean; | 39 | @Input() visible: boolean; |
38 | 40 | ||
41 | + @Input() style: { [klass: string]: any } = {}; | ||
42 | + | ||
39 | @Output() markdownReady = new EventEmitter<void>(); | 43 | @Output() markdownReady = new EventEmitter<void>(); |
40 | 44 | ||
41 | markdownText = new BehaviorSubject<string>(null); | 45 | markdownText = new BehaviorSubject<string>(null); |
@@ -44,8 +48,6 @@ export class HelpMarkdownComponent implements OnDestroy, OnInit, OnChanges { | @@ -44,8 +48,6 @@ export class HelpMarkdownComponent implements OnDestroy, OnInit, OnChanges { | ||
44 | share() | 48 | share() |
45 | ); | 49 | ); |
46 | 50 | ||
47 | - isMarkdownReady = false; | ||
48 | - | ||
49 | private loadHelpPending = false; | 51 | private loadHelpPending = false; |
50 | 52 | ||
51 | constructor(private help: HelpService) {} | 53 | constructor(private help: HelpService) {} |
@@ -68,7 +70,7 @@ export class HelpMarkdownComponent implements OnDestroy, OnInit, OnChanges { | @@ -68,7 +70,7 @@ export class HelpMarkdownComponent implements OnDestroy, OnInit, OnChanges { | ||
68 | this.loadHelp(); | 70 | this.loadHelp(); |
69 | } | 71 | } |
70 | } | 72 | } |
71 | - if (propName === 'helpId') { | 73 | + if (propName === 'helpId' || propName === 'helpContent') { |
72 | this.markdownText.next(null); | 74 | this.markdownText.next(null); |
73 | this.loadHelpWhenVisible(); | 75 | this.loadHelpWhenVisible(); |
74 | } | 76 | } |
@@ -89,16 +91,16 @@ export class HelpMarkdownComponent implements OnDestroy, OnInit, OnChanges { | @@ -89,16 +91,16 @@ export class HelpMarkdownComponent implements OnDestroy, OnInit, OnChanges { | ||
89 | this.help.getHelpContent(this.helpId).subscribe((content) => { | 91 | this.help.getHelpContent(this.helpId).subscribe((content) => { |
90 | this.markdownText.next(content); | 92 | this.markdownText.next(content); |
91 | }); | 93 | }); |
94 | + } else if (this.helpContent) { | ||
95 | + this.markdownText.next(this.helpContent); | ||
92 | } | 96 | } |
93 | } | 97 | } |
94 | 98 | ||
95 | onMarkdownReady() { | 99 | onMarkdownReady() { |
96 | - this.isMarkdownReady = true; | ||
97 | this.markdownReady.next(); | 100 | this.markdownReady.next(); |
98 | } | 101 | } |
99 | 102 | ||
100 | markdownClick($event: MouseEvent) { | 103 | markdownClick($event: MouseEvent) { |
101 | - | ||
102 | } | 104 | } |
103 | 105 | ||
104 | } | 106 | } |
@@ -15,16 +15,22 @@ | @@ -15,16 +15,22 @@ | ||
15 | limitations under the License. | 15 | limitations under the License. |
16 | 16 | ||
17 | --> | 17 | --> |
18 | -<div style="position: relative;"> | ||
19 | - <button color="primary" mat-button mat-icon-button | ||
20 | - class="tb-help-popup-button tb-mat-32" | ||
21 | - type="button" | ||
22 | - (click)="toggleHelp()" | ||
23 | - matTooltip="{{'help.show-help' | translate}}" | ||
24 | - matTooltipPosition="above"> | ||
25 | - <mat-icon class="material-icons">{{popoverVisible ? 'help' : 'help_outline'}}</mat-icon> | ||
26 | - </button> | ||
27 | - <div *ngIf="popoverVisible && !popoverReady" fxFlex fxLayoutAlign="center center" class="tb-absolute-fill tb-help-popup-button-loading"> | ||
28 | - <mat-spinner mode="indeterminate" diameter="20" strokeWidth="2"></mat-spinner> | ||
29 | - </div> | ||
30 | -</div> | 18 | +<button #toggleHelpButton |
19 | + *ngIf="!triggerText" | ||
20 | + color="primary" mat-button mat-icon-button | ||
21 | + class="tb-help-popup-button tb-mat-32" | ||
22 | + type="button" | ||
23 | + (click)="toggleHelp()" | ||
24 | + matTooltip="{{'help.show-help' | translate}}" | ||
25 | + matTooltipPosition="above"> | ||
26 | + <mat-icon class="material-icons">{{popoverVisible ? 'help' : 'help_outline'}}</mat-icon> | ||
27 | + <mat-spinner *ngIf="popoverVisible && !popoverReady" class="tb-help-popup-button-loading" mode="indeterminate" diameter="20" strokeWidth="2"></mat-spinner> | ||
28 | +</button> | ||
29 | +<span #toggleHelpSpan | ||
30 | + *ngIf="triggerText" | ||
31 | + class="tb-help-popup-span" | ||
32 | + (click)="toggleHelp()" | ||
33 | + [ngClass]="{'active': popoverVisible, 'ready': popoverReady}"> | ||
34 | + <span>{{triggerText}}</span> | ||
35 | + <mat-progress-bar *ngIf="popoverVisible && !popoverReady" color="primary" mode="indeterminate"></mat-progress-bar> | ||
36 | +</span> |
@@ -14,8 +14,54 @@ | @@ -14,8 +14,54 @@ | ||
14 | * limitations under the License. | 14 | * limitations under the License. |
15 | */ | 15 | */ |
16 | .tb-help-popup-button { | 16 | .tb-help-popup-button { |
17 | + position: relative; | ||
17 | } | 18 | } |
18 | .tb-help-popup-button-loading { | 19 | .tb-help-popup-button-loading { |
19 | background: #fff; | 20 | background: #fff; |
20 | border-radius: 50%; | 21 | border-radius: 50%; |
22 | + width: 32px !important; | ||
23 | + height: 32px !important; | ||
24 | + position: absolute; | ||
25 | + top: 0; | ||
26 | + left: 0; | ||
27 | + &.mat-progress-spinner { | ||
28 | + svg { | ||
29 | + top: 6px; | ||
30 | + left: 6px; | ||
31 | + } | ||
32 | + } | ||
33 | +} | ||
34 | + | ||
35 | +.tb-help-popup-span { | ||
36 | + position: relative; | ||
37 | + cursor: pointer; | ||
38 | + -webkit-user-select: none; | ||
39 | + -moz-user-select: none; | ||
40 | + -ms-user-select: none; | ||
41 | + user-select: none; | ||
42 | + color: #2a7dec; | ||
43 | + font-weight: 500; | ||
44 | + &.active.ready { | ||
45 | + text-decoration: underline; | ||
46 | + } | ||
47 | + &:hover { | ||
48 | + &:not(.active) { | ||
49 | + text-decoration: underline; | ||
50 | + } | ||
51 | + } | ||
52 | + &:after { | ||
53 | + display: inline-block; | ||
54 | + font-family: "FontAwesome"; | ||
55 | + content: '\f08e'; | ||
56 | + padding-left: 5px; | ||
57 | + font-size: 14px; | ||
58 | + font-weight: 600; | ||
59 | + } | ||
60 | + .mat-progress-bar { | ||
61 | + position: absolute; | ||
62 | + height: 2px; | ||
63 | + left: 0; | ||
64 | + right: 0; | ||
65 | + bottom: 0; | ||
66 | + } | ||
21 | } | 67 | } |
@@ -14,8 +14,18 @@ | @@ -14,8 +14,18 @@ | ||
14 | /// limitations under the License. | 14 | /// limitations under the License. |
15 | /// | 15 | /// |
16 | 16 | ||
17 | -import { Component, ElementRef, Input, OnDestroy, Renderer2, ViewContainerRef, ViewEncapsulation } from '@angular/core'; | ||
18 | -import { TbPopoverService } from '@shared/components/popover.component'; | 17 | +import { |
18 | + Component, | ||
19 | + ElementRef, | ||
20 | + Input, | ||
21 | + OnDestroy, | ||
22 | + Renderer2, | ||
23 | + ViewChild, | ||
24 | + ViewContainerRef, | ||
25 | + ViewEncapsulation | ||
26 | +} from '@angular/core'; | ||
27 | +import { TbPopoverService } from '@shared/components/popover.service'; | ||
28 | +import { PopoverPlacement } from '@shared/components/popover.models'; | ||
19 | 29 | ||
20 | @Component({ | 30 | @Component({ |
21 | // tslint:disable-next-line:component-selector | 31 | // tslint:disable-next-line:component-selector |
@@ -26,25 +36,41 @@ import { TbPopoverService } from '@shared/components/popover.component'; | @@ -26,25 +36,41 @@ import { TbPopoverService } from '@shared/components/popover.component'; | ||
26 | }) | 36 | }) |
27 | export class HelpPopupComponent implements OnDestroy { | 37 | export class HelpPopupComponent implements OnDestroy { |
28 | 38 | ||
39 | + @ViewChild('toggleHelpButton', {read: ElementRef, static: false}) toggleHelpButton: ElementRef; | ||
40 | + @ViewChild('toggleHelpSpan', {read: ElementRef, static: false}) toggleHelpSpan: ElementRef; | ||
41 | + | ||
29 | // tslint:disable-next-line:no-input-rename | 42 | // tslint:disable-next-line:no-input-rename |
30 | @Input('tb-help-popup') helpId: string; | 43 | @Input('tb-help-popup') helpId: string; |
31 | 44 | ||
45 | + // tslint:disable-next-line:no-input-rename | ||
46 | + @Input('trigger-text') triggerText: string; | ||
47 | + | ||
48 | + // tslint:disable-next-line:no-input-rename | ||
49 | + @Input('tb-help-popup-placement') helpPopupPlacement: PopoverPlacement; | ||
50 | + | ||
51 | + // tslint:disable-next-line:no-input-rename | ||
52 | + @Input('tb-help-popup-style') helpPopupStyle: { [klass: string]: any } = {}; | ||
53 | + | ||
32 | popoverVisible = false; | 54 | popoverVisible = false; |
33 | popoverReady = true; | 55 | popoverReady = true; |
34 | 56 | ||
35 | - constructor(private elementRef: ElementRef, | ||
36 | - private viewContainerRef: ViewContainerRef, | 57 | + constructor(private viewContainerRef: ViewContainerRef, |
37 | private renderer: Renderer2, | 58 | private renderer: Renderer2, |
38 | private popoverService: TbPopoverService) {} | 59 | private popoverService: TbPopoverService) {} |
39 | 60 | ||
40 | toggleHelp() { | 61 | toggleHelp() { |
41 | - this.popoverService.toggleHelpPopover(this.elementRef.nativeElement, this.renderer, this.viewContainerRef, | 62 | + const trigger = this.triggerText ? this.toggleHelpSpan.nativeElement : this.toggleHelpButton.nativeElement; |
63 | + this.popoverService.toggleHelpPopover(trigger, this.renderer, this.viewContainerRef, | ||
42 | this.helpId, | 64 | this.helpId, |
65 | + '', | ||
43 | (visible) => { | 66 | (visible) => { |
44 | this.popoverVisible = visible; | 67 | this.popoverVisible = visible; |
45 | }, (ready => { | 68 | }, (ready => { |
46 | this.popoverReady = ready; | 69 | this.popoverReady = ready; |
47 | - })); | 70 | + }), |
71 | + this.helpPopupPlacement, | ||
72 | + {}, | ||
73 | + this.helpPopupStyle); | ||
48 | } | 74 | } |
49 | 75 | ||
50 | ngOnDestroy(): void { | 76 | ngOnDestroy(): void { |
@@ -46,8 +46,7 @@ import { GroupInfo } from '@shared/models/widget.models'; | @@ -46,8 +46,7 @@ import { GroupInfo } from '@shared/models/widget.models'; | ||
46 | import { Observable } from 'rxjs/internal/Observable'; | 46 | import { Observable } from 'rxjs/internal/Observable'; |
47 | import { forkJoin, from } from 'rxjs'; | 47 | import { forkJoin, from } from 'rxjs'; |
48 | import { MouseEvent } from 'react'; | 48 | import { MouseEvent } from 'react'; |
49 | -import { TbPopoverService } from '@shared/components/popover.component'; | ||
50 | -import { HelpMarkdownComponent } from '@shared/components/help-markdown.component'; | 49 | +import { TbPopoverService } from '@shared/components/popover.service'; |
51 | 50 | ||
52 | const tinycolor = tinycolor_; | 51 | const tinycolor = tinycolor_; |
53 | 52 | ||
@@ -252,7 +251,7 @@ export class JsonFormComponent implements OnInit, ControlValueAccessor, Validato | @@ -252,7 +251,7 @@ export class JsonFormComponent implements OnInit, ControlValueAccessor, Validato | ||
252 | 251 | ||
253 | private onHelpClick(event: MouseEvent, helpId: string, helpVisibleFn: (visible: boolean) => void, helpReadyFn: (ready: boolean) => void) { | 252 | private onHelpClick(event: MouseEvent, helpId: string, helpVisibleFn: (visible: boolean) => void, helpReadyFn: (ready: boolean) => void) { |
254 | const trigger = event.currentTarget as Element; | 253 | const trigger = event.currentTarget as Element; |
255 | - this.popoverService.toggleHelpPopover(trigger, this.renderer, this.viewContainerRef, helpId, helpVisibleFn, helpReadyFn); | 254 | + this.popoverService.toggleHelpPopover(trigger, this.renderer, this.viewContainerRef, helpId, '', helpVisibleFn, helpReadyFn); |
256 | } | 255 | } |
257 | 256 | ||
258 | private updateAndRender() { | 257 | private updateAndRender() { |
1 | +<!-- | ||
2 | + | ||
3 | + Copyright © 2016-2021 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 | +<ng-container #markdownContainer> | ||
19 | + <markdown *ngIf="!isMarkdownReady" #markdownComponent [data]="data" | ||
20 | + [lineNumbers]="lineNumbers" (ready)="onMarkdownReady()"></markdown> | ||
21 | +</ng-container> |
1 | +/** | ||
2 | + * Copyright © 2016-2021 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 | + markdown { | ||
18 | + display: none; | ||
19 | + } | ||
20 | +} |
1 | +/// | ||
2 | +/// Copyright © 2016-2021 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 | + ComponentFactory, | ||
20 | + ComponentRef, | ||
21 | + EventEmitter, | ||
22 | + Inject, | ||
23 | + Injector, | ||
24 | + Input, OnChanges, | ||
25 | + OnDestroy, | ||
26 | + OnInit, | ||
27 | + Output, | ||
28 | + Renderer2, SimpleChanges, | ||
29 | + Type, | ||
30 | + ViewChild, | ||
31 | + ViewContainerRef | ||
32 | +} from '@angular/core'; | ||
33 | +import { HelpService } from '@core/services/help.service'; | ||
34 | +import { MarkdownComponent } from 'ngx-markdown'; | ||
35 | +import { DynamicComponentFactoryService } from '@core/services/dynamic-component-factory.service'; | ||
36 | +import { coerceBooleanProperty } from '@angular/cdk/coercion'; | ||
37 | +import { SHARED_MODULE_TOKEN } from '@shared/components/tokens'; | ||
38 | + | ||
39 | +@Component({ | ||
40 | + selector: 'tb-markdown', | ||
41 | + templateUrl: './markdown.component.html', | ||
42 | + styleUrls: ['./markdown.component.scss'] | ||
43 | +}) | ||
44 | +export class TbMarkdownComponent implements OnDestroy, OnInit, OnChanges { | ||
45 | + | ||
46 | + private markdownComponent: MarkdownComponent; | ||
47 | + | ||
48 | + @ViewChild('markdownContainer', {read: ViewContainerRef, static: true}) markdownContainer: ViewContainerRef; | ||
49 | + | ||
50 | + @ViewChild('markdownComponent', {static: false}) set content(content: MarkdownComponent) { | ||
51 | + this.markdownComponent = content; | ||
52 | + if (this.isMarkdownReady && this.markdownComponent) { | ||
53 | + this.processMarkdownComponent(); | ||
54 | + } | ||
55 | + } | ||
56 | + | ||
57 | + @Input() data: string | undefined; | ||
58 | + | ||
59 | + @Input() markdownClass: string | undefined; | ||
60 | + | ||
61 | + @Input() style: { [klass: string]: any } = {}; | ||
62 | + | ||
63 | + @Input() | ||
64 | + get lineNumbers(): boolean { return this.lineNumbersValue; } | ||
65 | + set lineNumbers(value: boolean) { this.lineNumbersValue = coerceBooleanProperty(value); } | ||
66 | + | ||
67 | + @Output() ready = new EventEmitter<void>(); | ||
68 | + | ||
69 | + private lineNumbersValue = false; | ||
70 | + | ||
71 | + isMarkdownReady = false; | ||
72 | + | ||
73 | + private tbMarkdownInstanceComponentRef: ComponentRef<any>; | ||
74 | + private tbMarkdownInstanceComponentFactory: ComponentFactory<any>; | ||
75 | + | ||
76 | + constructor(private help: HelpService, | ||
77 | + private renderer: Renderer2, | ||
78 | + @Inject(SHARED_MODULE_TOKEN) private sharedModule: Type<any>, | ||
79 | + private dynamicComponentFactoryService: DynamicComponentFactoryService) {} | ||
80 | + | ||
81 | + ngOnInit(): void { | ||
82 | + } | ||
83 | + | ||
84 | + ngOnDestroy(): void { | ||
85 | + } | ||
86 | + | ||
87 | + ngOnChanges(changes: SimpleChanges): void { | ||
88 | + for (const propName of Object.keys(changes)) { | ||
89 | + const change = changes[propName]; | ||
90 | + if (!change.firstChange && change.currentValue !== change.previousValue) { | ||
91 | + if (propName === 'data') { | ||
92 | + if (this.data) { | ||
93 | + this.isMarkdownReady = false; | ||
94 | + } | ||
95 | + } | ||
96 | + } | ||
97 | + } | ||
98 | + } | ||
99 | + | ||
100 | + private processMarkdownComponent() { | ||
101 | + let template = this.markdownComponent.element.nativeElement.innerHTML; | ||
102 | + template = this.sanitizeCurlyBraces(template); | ||
103 | + let markdownClass = 'tb-markdown-view'; | ||
104 | + if (this.markdownClass) { | ||
105 | + markdownClass += ` ${this.markdownClass}`; | ||
106 | + } | ||
107 | + template = `<div [ngStyle]="style" class="${markdownClass}">${template}</div>`; | ||
108 | + this.markdownContainer.clear(); | ||
109 | + this.markdownComponent = null; | ||
110 | + const parent = this; | ||
111 | + this.dynamicComponentFactoryService.createDynamicComponentFactory( | ||
112 | + class TbMarkdownInstance { | ||
113 | + ngOnDestroy(): void { | ||
114 | + parent.destroyMarkdownInstanceResources(); | ||
115 | + } | ||
116 | + }, | ||
117 | + template, | ||
118 | + [this.sharedModule], | ||
119 | + true | ||
120 | + ).subscribe((factory) => { | ||
121 | + this.tbMarkdownInstanceComponentFactory = factory; | ||
122 | + const injector: Injector = Injector.create({providers: [], parent: this.markdownContainer.injector}); | ||
123 | + try { | ||
124 | + this.tbMarkdownInstanceComponentRef = | ||
125 | + this.markdownContainer.createComponent(this.tbMarkdownInstanceComponentFactory, 0, injector); | ||
126 | + this.tbMarkdownInstanceComponentRef.instance.style = this.style; | ||
127 | + } catch (e) { | ||
128 | + this.destroyMarkdownInstanceResources(); | ||
129 | + } | ||
130 | + this.ready.emit(); | ||
131 | + }); | ||
132 | + } | ||
133 | + | ||
134 | + private sanitizeCurlyBraces(template: string): string { | ||
135 | + return template.replace(/{/g, '{').replace(/}/g, '}'); | ||
136 | + } | ||
137 | + | ||
138 | + private destroyMarkdownInstanceResources() { | ||
139 | + if (this.tbMarkdownInstanceComponentFactory) { | ||
140 | + this.dynamicComponentFactoryService.destroyDynamicComponentFactory(this.tbMarkdownInstanceComponentFactory); | ||
141 | + this.tbMarkdownInstanceComponentFactory = null; | ||
142 | + } | ||
143 | + this.tbMarkdownInstanceComponentRef = null; | ||
144 | + } | ||
145 | + | ||
146 | + onMarkdownReady() { | ||
147 | + if (this.data) { | ||
148 | + this.isMarkdownReady = true; | ||
149 | + if (this.markdownComponent) { | ||
150 | + this.processMarkdownComponent(); | ||
151 | + } | ||
152 | + } | ||
153 | + } | ||
154 | +} |
@@ -85,7 +85,7 @@ export class MarkedOptionsService extends MarkedOptions { | @@ -85,7 +85,7 @@ export class MarkedOptionsService extends MarkedOptions { | ||
85 | 85 | ||
86 | private wrapCopyCode(id: number, content: string, code: string): string { | 86 | private wrapCopyCode(id: number, content: string, code: string): string { |
87 | return `<div class="code-wrapper noChars" id="codeWrapper${id}" onClick="markdownCopyCode(${id})">${content}` + | 87 | return `<div class="code-wrapper noChars" id="codeWrapper${id}" onClick="markdownCopyCode(${id})">${content}` + |
88 | - `<span id="copyCodeId${id}" style="display: none;">${code}</span>` + | 88 | + `<span id="copyCodeId${id}" style="display: none;">${encodeURIComponent(code)}</span>` + |
89 | `<button class="clipboard-btn">\n` + | 89 | `<button class="clipboard-btn">\n` + |
90 | ` <p>${this.translate.instant('markdown.copy-code')}</p>\n` + | 90 | ` <p>${this.translate.instant('markdown.copy-code')}</p>\n` + |
91 | ` <div>\n` + | 91 | ` <div>\n` + |
@@ -119,7 +119,7 @@ export class MarkedOptionsService extends MarkedOptions { | @@ -119,7 +119,7 @@ export class MarkedOptionsService extends MarkedOptions { | ||
119 | private markdownCopyCode(id: number) { | 119 | private markdownCopyCode(id: number) { |
120 | const copyWrapper = $('#codeWrapper' + id); | 120 | const copyWrapper = $('#codeWrapper' + id); |
121 | if (copyWrapper.hasClass('noChars')) { | 121 | if (copyWrapper.hasClass('noChars')) { |
122 | - const text = $('#copyCodeId' + id).text(); | 122 | + const text = decodeURIComponent($('#copyCodeId' + id).text()); |
123 | this.window.navigator.clipboard.writeText(text).then(() => { | 123 | this.window.navigator.clipboard.writeText(text).then(() => { |
124 | import('tooltipster').then( | 124 | import('tooltipster').then( |
125 | () => { | 125 | () => { |
@@ -25,7 +25,6 @@ import { | @@ -25,7 +25,6 @@ import { | ||
25 | Directive, | 25 | Directive, |
26 | ElementRef, | 26 | ElementRef, |
27 | EventEmitter, | 27 | EventEmitter, |
28 | - Injectable, | ||
29 | Injector, | 28 | Injector, |
30 | Input, | 29 | Input, |
31 | OnChanges, | 30 | OnChanges, |
@@ -36,7 +35,6 @@ import { | @@ -36,7 +35,6 @@ import { | ||
36 | Renderer2, | 35 | Renderer2, |
37 | SimpleChanges, | 36 | SimpleChanges, |
38 | TemplateRef, | 37 | TemplateRef, |
39 | - Type, | ||
40 | ViewChild, | 38 | ViewChild, |
41 | ViewContainerRef, | 39 | ViewContainerRef, |
42 | ViewEncapsulation | 40 | ViewEncapsulation |
@@ -54,13 +52,11 @@ import { | @@ -54,13 +52,11 @@ import { | ||
54 | getPlacementName, | 52 | getPlacementName, |
55 | popoverMotion, | 53 | popoverMotion, |
56 | PopoverPlacement, | 54 | PopoverPlacement, |
57 | - PopoverWithTrigger, | ||
58 | POSITION_MAP, | 55 | POSITION_MAP, |
59 | PropertyMapping | 56 | PropertyMapping |
60 | } from '@shared/components/popover.models'; | 57 | } from '@shared/components/popover.models'; |
61 | import { distinctUntilChanged, takeUntil } from 'rxjs/operators'; | 58 | import { distinctUntilChanged, takeUntil } from 'rxjs/operators'; |
62 | import { isNotEmptyStr, onParentScrollOrWindowResize } from '@core/utils'; | 59 | import { isNotEmptyStr, onParentScrollOrWindowResize } from '@core/utils'; |
63 | -import { HelpMarkdownComponent } from '@shared/components/help-markdown.component'; | ||
64 | 60 | ||
65 | export type TbPopoverTrigger = 'click' | 'focus' | 'hover' | null; | 61 | export type TbPopoverTrigger = 'click' | 'focus' | 'hover' | null; |
66 | 62 | ||
@@ -285,162 +281,6 @@ export class TbPopoverDirective implements OnChanges, OnDestroy, AfterViewInit { | @@ -285,162 +281,6 @@ export class TbPopoverDirective implements OnChanges, OnDestroy, AfterViewInit { | ||
285 | } | 281 | } |
286 | } | 282 | } |
287 | 283 | ||
288 | -@Injectable() | ||
289 | -export class TbPopoverService { | ||
290 | - | ||
291 | - private popoverWithTriggers: PopoverWithTrigger[] = []; | ||
292 | - | ||
293 | - componentFactory: ComponentFactory<TbPopoverComponent> = this.resolver.resolveComponentFactory(TbPopoverComponent); | ||
294 | - | ||
295 | - constructor(private resolver: ComponentFactoryResolver) { | ||
296 | - } | ||
297 | - | ||
298 | - hasPopover(trigger: Element): boolean { | ||
299 | - const res = this.findPopoverByTrigger(trigger); | ||
300 | - return res !== null; | ||
301 | - } | ||
302 | - | ||
303 | - hidePopover(trigger: Element): boolean { | ||
304 | - const component: TbPopoverComponent = this.findPopoverByTrigger(trigger); | ||
305 | - if (component && component.tbVisible) { | ||
306 | - component.hide(); | ||
307 | - return true; | ||
308 | - } else { | ||
309 | - return false; | ||
310 | - } | ||
311 | - } | ||
312 | - | ||
313 | - displayPopover<T>(trigger: Element, renderer: Renderer2, hostView: ViewContainerRef, | ||
314 | - componentType: Type<T>, preferredPlacement: PopoverPlacement = 'top', hideOnClickOutside = true, | ||
315 | - injector?: Injector, context?: any, overlayStyle: any = {}, popoverStyle: any = {}, style?: any): TbPopoverComponent { | ||
316 | - const componentRef = hostView.createComponent(this.componentFactory); | ||
317 | - const component = componentRef.instance; | ||
318 | - this.popoverWithTriggers.push({ | ||
319 | - trigger, | ||
320 | - popoverComponent: component | ||
321 | - }); | ||
322 | - renderer.removeChild( | ||
323 | - renderer.parentNode(trigger), | ||
324 | - componentRef.location.nativeElement | ||
325 | - ); | ||
326 | - const originElementRef = new ElementRef(trigger); | ||
327 | - component.setOverlayOrigin({ elementRef: originElementRef }); | ||
328 | - component.tbPlacement = preferredPlacement; | ||
329 | - component.tbComponentFactory = this.resolver.resolveComponentFactory(componentType); | ||
330 | - component.tbComponentInjector = injector; | ||
331 | - component.tbComponentContext = context; | ||
332 | - component.tbOverlayStyle = overlayStyle; | ||
333 | - component.tbPopoverInnerStyle = popoverStyle; | ||
334 | - component.tbComponentStyle = style; | ||
335 | - component.tbHideOnClickOutside = hideOnClickOutside; | ||
336 | - component.tbVisibleChange.subscribe((visible: boolean) => { | ||
337 | - if (!visible) { | ||
338 | - component.tbAnimationDone.subscribe(() => { | ||
339 | - componentRef.destroy(); | ||
340 | - }); | ||
341 | - } | ||
342 | - }); | ||
343 | - component.tbDestroy.subscribe(() => { | ||
344 | - this.removePopoverByComponent(component); | ||
345 | - }); | ||
346 | - component.show(); | ||
347 | - return component; | ||
348 | - } | ||
349 | - | ||
350 | - toggleHelpPopover(trigger: Element, renderer: Renderer2, hostView: ViewContainerRef, helpId: string, | ||
351 | - visibleFn: (visible: boolean) => void, readyFn: (ready: boolean) => void) { | ||
352 | - if (this.hasPopover(trigger)) { | ||
353 | - this.hidePopover(trigger); | ||
354 | - } else { | ||
355 | - readyFn(false); | ||
356 | - const injector = Injector.create({ | ||
357 | - parent: hostView.injector, providers: [] | ||
358 | - }); | ||
359 | - const componentRef = hostView.createComponent(this.componentFactory); | ||
360 | - const component = componentRef.instance; | ||
361 | - this.popoverWithTriggers.push({ | ||
362 | - trigger, | ||
363 | - popoverComponent: component | ||
364 | - }); | ||
365 | - renderer.removeChild( | ||
366 | - renderer.parentNode(trigger), | ||
367 | - componentRef.location.nativeElement | ||
368 | - ); | ||
369 | - const originElementRef = new ElementRef(trigger); | ||
370 | - component.tbAnimationState = 'void'; | ||
371 | - component.tbOverlayStyle = { opacity: '0' }; | ||
372 | - component.setOverlayOrigin({ elementRef: originElementRef }); | ||
373 | - component.tbPlacement = 'bottom'; | ||
374 | - component.tbComponentFactory = this.resolver.resolveComponentFactory(HelpMarkdownComponent); | ||
375 | - component.tbComponentInjector = injector; | ||
376 | - component.tbComponentContext = { | ||
377 | - helpId, | ||
378 | - visible: true | ||
379 | - }; | ||
380 | - component.tbHideOnClickOutside = true; | ||
381 | - component.tbVisibleChange.subscribe((visible: boolean) => { | ||
382 | - if (!visible) { | ||
383 | - visibleFn(false); | ||
384 | - component.tbAnimationDone.subscribe(() => { | ||
385 | - componentRef.destroy(); | ||
386 | - }); | ||
387 | - } | ||
388 | - }); | ||
389 | - component.tbDestroy.subscribe(() => { | ||
390 | - this.removePopoverByComponent(component); | ||
391 | - }); | ||
392 | - const showHelpMarkdownComponent = () => { | ||
393 | - component.tbOverlayStyle = { opacity: '1' }; | ||
394 | - component.tbAnimationState = 'active'; | ||
395 | - component.updatePosition(); | ||
396 | - readyFn(true); | ||
397 | - setTimeout(() => { | ||
398 | - component.updatePosition(); | ||
399 | - }); | ||
400 | - }; | ||
401 | - const setupHelpMarkdownComponent = (helpMarkdownComponent: HelpMarkdownComponent) => { | ||
402 | - if (helpMarkdownComponent.isMarkdownReady) { | ||
403 | - showHelpMarkdownComponent(); | ||
404 | - } else { | ||
405 | - helpMarkdownComponent.markdownReady.subscribe(() => { | ||
406 | - showHelpMarkdownComponent(); | ||
407 | - }); | ||
408 | - } | ||
409 | - }; | ||
410 | - if (component.tbComponentRef) { | ||
411 | - setupHelpMarkdownComponent(component.tbComponentRef.instance); | ||
412 | - } else { | ||
413 | - component.tbComponentChange.subscribe((helpMarkdownComponentRef) => { | ||
414 | - setupHelpMarkdownComponent(helpMarkdownComponentRef.instance); | ||
415 | - }); | ||
416 | - } | ||
417 | - component.show(); | ||
418 | - visibleFn(true); | ||
419 | - } | ||
420 | - } | ||
421 | - | ||
422 | - private findPopoverByTrigger(trigger: Element): TbPopoverComponent | null { | ||
423 | - const res = this.popoverWithTriggers.find(val => this.elementsAreEqualOrDescendant(trigger, val.trigger)); | ||
424 | - if (res) { | ||
425 | - return res.popoverComponent; | ||
426 | - } else { | ||
427 | - return null; | ||
428 | - } | ||
429 | - } | ||
430 | - | ||
431 | - private removePopoverByComponent(component: TbPopoverComponent): void { | ||
432 | - const index = this.popoverWithTriggers.findIndex(val => val.popoverComponent === component); | ||
433 | - if (index > -1) { | ||
434 | - this.popoverWithTriggers.splice(index, 1); | ||
435 | - } | ||
436 | - } | ||
437 | - | ||
438 | - private elementsAreEqualOrDescendant(element1: Element, element2: Element): boolean { | ||
439 | - return element1 === element2 || element1.contains(element2) || element2.contains(element1); | ||
440 | - } | ||
441 | -} | ||
442 | - | ||
443 | - | ||
444 | @Component({ | 284 | @Component({ |
445 | selector: 'tb-popover', | 285 | selector: 'tb-popover', |
446 | exportAs: 'tbPopoverComponent', | 286 | exportAs: 'tbPopoverComponent', |
@@ -703,10 +543,12 @@ export class TbPopoverComponent implements OnDestroy, OnInit { | @@ -703,10 +543,12 @@ export class TbPopoverComponent implements OnDestroy, OnInit { | ||
703 | 543 | ||
704 | updateStyles(): void { | 544 | updateStyles(): void { |
705 | this.classMap = { | 545 | this.classMap = { |
706 | - [this.tbOverlayClassName]: true, | ||
707 | [`tb-popover-placement-${this.preferredPlacement}`]: true, | 546 | [`tb-popover-placement-${this.preferredPlacement}`]: true, |
708 | ['tb-popover-hidden']: this.tbHidden || !this.lastIsIntersecting | 547 | ['tb-popover-hidden']: this.tbHidden || !this.lastIsIntersecting |
709 | }; | 548 | }; |
549 | + if (this.tbOverlayClassName) { | ||
550 | + this.classMap[this.tbOverlayClassName] = true; | ||
551 | + } | ||
710 | } | 552 | } |
711 | 553 | ||
712 | setOverlayOrigin(origin: CdkOverlayOrigin): void { | 554 | setOverlayOrigin(origin: CdkOverlayOrigin): void { |
1 | +/// | ||
2 | +/// Copyright © 2016-2021 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 | + ComponentFactory, | ||
19 | + ComponentFactoryResolver, ElementRef, Inject, | ||
20 | + Injectable, Injector, | ||
21 | + Renderer2, | ||
22 | + Type, | ||
23 | + ViewContainerRef | ||
24 | +} from '@angular/core'; | ||
25 | +import { PopoverPlacement, PopoverWithTrigger } from '@shared/components/popover.models'; | ||
26 | +import { TbPopoverComponent } from '@shared/components/popover.component'; | ||
27 | +import { ComponentType } from '@angular/cdk/portal'; | ||
28 | +import { HELP_MARKDOWN_COMPONENT_TOKEN } from '@shared/components/tokens'; | ||
29 | + | ||
30 | +@Injectable() | ||
31 | +export class TbPopoverService { | ||
32 | + | ||
33 | + private popoverWithTriggers: PopoverWithTrigger[] = []; | ||
34 | + | ||
35 | + componentFactory: ComponentFactory<TbPopoverComponent> = this.resolver.resolveComponentFactory(TbPopoverComponent); | ||
36 | + | ||
37 | + constructor(private resolver: ComponentFactoryResolver, | ||
38 | + @Inject(HELP_MARKDOWN_COMPONENT_TOKEN) private helpMarkdownComponent: ComponentType<any>) { | ||
39 | + } | ||
40 | + | ||
41 | + hasPopover(trigger: Element): boolean { | ||
42 | + const res = this.findPopoverByTrigger(trigger); | ||
43 | + return res !== null; | ||
44 | + } | ||
45 | + | ||
46 | + hidePopover(trigger: Element): boolean { | ||
47 | + const component: TbPopoverComponent = this.findPopoverByTrigger(trigger); | ||
48 | + if (component && component.tbVisible) { | ||
49 | + component.hide(); | ||
50 | + return true; | ||
51 | + } else { | ||
52 | + return false; | ||
53 | + } | ||
54 | + } | ||
55 | + | ||
56 | + displayPopover<T>(trigger: Element, renderer: Renderer2, hostView: ViewContainerRef, | ||
57 | + componentType: Type<T>, preferredPlacement: PopoverPlacement = 'top', hideOnClickOutside = true, | ||
58 | + injector?: Injector, context?: any, overlayStyle: any = {}, popoverStyle: any = {}, style?: any): TbPopoverComponent { | ||
59 | + const componentRef = hostView.createComponent(this.componentFactory); | ||
60 | + const component = componentRef.instance; | ||
61 | + this.popoverWithTriggers.push({ | ||
62 | + trigger, | ||
63 | + popoverComponent: component | ||
64 | + }); | ||
65 | + renderer.removeChild( | ||
66 | + renderer.parentNode(trigger), | ||
67 | + componentRef.location.nativeElement | ||
68 | + ); | ||
69 | + const originElementRef = new ElementRef(trigger); | ||
70 | + component.setOverlayOrigin({ elementRef: originElementRef }); | ||
71 | + component.tbPlacement = preferredPlacement; | ||
72 | + component.tbComponentFactory = this.resolver.resolveComponentFactory(componentType); | ||
73 | + component.tbComponentInjector = injector; | ||
74 | + component.tbComponentContext = context; | ||
75 | + component.tbOverlayStyle = overlayStyle; | ||
76 | + component.tbPopoverInnerStyle = popoverStyle; | ||
77 | + component.tbComponentStyle = style; | ||
78 | + component.tbHideOnClickOutside = hideOnClickOutside; | ||
79 | + component.tbVisibleChange.subscribe((visible: boolean) => { | ||
80 | + if (!visible) { | ||
81 | + component.tbAnimationDone.subscribe(() => { | ||
82 | + componentRef.destroy(); | ||
83 | + }); | ||
84 | + } | ||
85 | + }); | ||
86 | + component.tbDestroy.subscribe(() => { | ||
87 | + this.removePopoverByComponent(component); | ||
88 | + }); | ||
89 | + component.show(); | ||
90 | + return component; | ||
91 | + } | ||
92 | + | ||
93 | + toggleHelpPopover(trigger: Element, renderer: Renderer2, hostView: ViewContainerRef, helpId = '', | ||
94 | + helpContent = '', | ||
95 | + visibleFn: (visible: boolean) => void = () => {}, | ||
96 | + readyFn: (ready: boolean) => void = () => {}, | ||
97 | + preferredPlacement: PopoverPlacement = 'bottom', | ||
98 | + overlayStyle: any = {}, helpStyle: any = {}) { | ||
99 | + if (this.hasPopover(trigger)) { | ||
100 | + this.hidePopover(trigger); | ||
101 | + } else { | ||
102 | + readyFn(false); | ||
103 | + const injector = Injector.create({ | ||
104 | + parent: hostView.injector, providers: [] | ||
105 | + }); | ||
106 | + const componentRef = hostView.createComponent(this.componentFactory); | ||
107 | + const component = componentRef.instance; | ||
108 | + this.popoverWithTriggers.push({ | ||
109 | + trigger, | ||
110 | + popoverComponent: component | ||
111 | + }); | ||
112 | + renderer.removeChild( | ||
113 | + renderer.parentNode(trigger), | ||
114 | + componentRef.location.nativeElement | ||
115 | + ); | ||
116 | + const originElementRef = new ElementRef(trigger); | ||
117 | + component.tbAnimationState = 'void'; | ||
118 | + component.tbOverlayStyle = {...overlayStyle, opacity: '0' }; | ||
119 | + component.setOverlayOrigin({ elementRef: originElementRef }); | ||
120 | + component.tbPlacement = preferredPlacement; | ||
121 | + component.tbComponentFactory = this.resolver.resolveComponentFactory(this.helpMarkdownComponent); | ||
122 | + component.tbComponentInjector = injector; | ||
123 | + component.tbComponentContext = { | ||
124 | + helpId, | ||
125 | + helpContent, | ||
126 | + style: helpStyle, | ||
127 | + visible: true | ||
128 | + }; | ||
129 | + component.tbHideOnClickOutside = true; | ||
130 | + component.tbVisibleChange.subscribe((visible: boolean) => { | ||
131 | + if (!visible) { | ||
132 | + visibleFn(false); | ||
133 | + component.tbAnimationDone.subscribe(() => { | ||
134 | + componentRef.destroy(); | ||
135 | + }); | ||
136 | + } | ||
137 | + }); | ||
138 | + component.tbDestroy.subscribe(() => { | ||
139 | + this.removePopoverByComponent(component); | ||
140 | + }); | ||
141 | + const showHelpMarkdownComponent = () => { | ||
142 | + component.tbOverlayStyle = {...component.tbOverlayStyle, opacity: '1' }; | ||
143 | + component.tbAnimationState = 'active'; | ||
144 | + component.updatePosition(); | ||
145 | + readyFn(true); | ||
146 | + setTimeout(() => { | ||
147 | + component.updatePosition(); | ||
148 | + }); | ||
149 | + }; | ||
150 | + const setupHelpMarkdownComponent = (helpMarkdownComponent: any) => { | ||
151 | + if (helpMarkdownComponent.isMarkdownReady) { | ||
152 | + showHelpMarkdownComponent(); | ||
153 | + } else { | ||
154 | + helpMarkdownComponent.markdownReady.subscribe(() => { | ||
155 | + showHelpMarkdownComponent(); | ||
156 | + }); | ||
157 | + } | ||
158 | + }; | ||
159 | + if (component.tbComponentRef) { | ||
160 | + setupHelpMarkdownComponent(component.tbComponentRef.instance); | ||
161 | + } else { | ||
162 | + component.tbComponentChange.subscribe((helpMarkdownComponentRef) => { | ||
163 | + setupHelpMarkdownComponent(helpMarkdownComponentRef.instance); | ||
164 | + }); | ||
165 | + } | ||
166 | + component.show(); | ||
167 | + visibleFn(true); | ||
168 | + } | ||
169 | + } | ||
170 | + | ||
171 | + private findPopoverByTrigger(trigger: Element): TbPopoverComponent | null { | ||
172 | + const res = this.popoverWithTriggers.find(val => this.elementsAreEqualOrDescendant(trigger, val.trigger)); | ||
173 | + if (res) { | ||
174 | + return res.popoverComponent; | ||
175 | + } else { | ||
176 | + return null; | ||
177 | + } | ||
178 | + } | ||
179 | + | ||
180 | + private removePopoverByComponent(component: TbPopoverComponent): void { | ||
181 | + const index = this.popoverWithTriggers.findIndex(val => val.popoverComponent === component); | ||
182 | + if (index > -1) { | ||
183 | + this.popoverWithTriggers.splice(index, 1); | ||
184 | + } | ||
185 | + } | ||
186 | + | ||
187 | + private elementsAreEqualOrDescendant(element1: Element, element2: Element): boolean { | ||
188 | + return element1 === element2 || element1.contains(element2) || element2.contains(element1); | ||
189 | + } | ||
190 | +} |
ui-ngx/src/app/shared/components/tokens.ts
0 → 100644
1 | +/// | ||
2 | +/// Copyright © 2016-2021 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 { InjectionToken, Type } from '@angular/core'; | ||
18 | +import { ComponentType } from '@angular/cdk/portal'; | ||
19 | + | ||
20 | +export const HELP_MARKDOWN_COMPONENT_TOKEN: InjectionToken<ComponentType<any>> = | ||
21 | + new InjectionToken<ComponentType<any>>('HELP_MARKDOWN_COMPONENT_TOKEN'); | ||
22 | + | ||
23 | +export const SHARED_MODULE_TOKEN: InjectionToken<Type<any>> = | ||
24 | + new InjectionToken<Type<any>>('HELP_MARKDOWN_COMPONENT_TOKEN'); |
@@ -54,7 +54,7 @@ export const MediaBreakpoints = { | @@ -54,7 +54,7 @@ export const MediaBreakpoints = { | ||
54 | 'gt-xl': 'screen and (min-width: 5001px)' | 54 | 'gt-xl': 'screen and (min-width: 5001px)' |
55 | }; | 55 | }; |
56 | 56 | ||
57 | -const helpBaseUrl = 'https://thingsboard.io'; | 57 | +export const helpBaseUrl = 'https://thingsboard.io'; |
58 | 58 | ||
59 | export const HelpLinks = { | 59 | export const HelpLinks = { |
60 | linksMap: { | 60 | linksMap: { |
@@ -147,11 +147,14 @@ import { MAT_DATE_LOCALE } from '@angular/material/core'; | @@ -147,11 +147,14 @@ import { MAT_DATE_LOCALE } from '@angular/material/core'; | ||
147 | import { CopyButtonComponent } from '@shared/components/button/copy-button.component'; | 147 | import { CopyButtonComponent } from '@shared/components/button/copy-button.component'; |
148 | import { TogglePasswordComponent } from '@shared/components/button/toggle-password.component'; | 148 | import { TogglePasswordComponent } from '@shared/components/button/toggle-password.component'; |
149 | import { HelpPopupComponent } from '@shared/components/help-popup.component'; | 149 | import { HelpPopupComponent } from '@shared/components/help-popup.component'; |
150 | -import { TbPopoverComponent, TbPopoverDirective, TbPopoverService } from '@shared/components/popover.component'; | 150 | +import { TbPopoverComponent, TbPopoverDirective } from '@shared/components/popover.component'; |
151 | import { TbStringTemplateOutletDirective } from '@shared/components/directives/sring-template-outlet.directive'; | 151 | import { TbStringTemplateOutletDirective } from '@shared/components/directives/sring-template-outlet.directive'; |
152 | import { TbComponentOutletDirective} from '@shared/components/directives/component-outlet.directive'; | 152 | import { TbComponentOutletDirective} from '@shared/components/directives/component-outlet.directive'; |
153 | import { HelpMarkdownComponent } from '@shared/components/help-markdown.component'; | 153 | import { HelpMarkdownComponent } from '@shared/components/help-markdown.component'; |
154 | import { MarkedOptionsService } from '@shared/components/marked-options.service'; | 154 | import { MarkedOptionsService } from '@shared/components/marked-options.service'; |
155 | +import { TbPopoverService } from '@shared/components/popover.service'; | ||
156 | +import { HELP_MARKDOWN_COMPONENT_TOKEN, SHARED_MODULE_TOKEN } from '@shared/components/tokens'; | ||
157 | +import { TbMarkdownComponent } from '@shared/components/markdown.component'; | ||
155 | 158 | ||
156 | export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) { | 159 | export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) { |
157 | return markedOptionsService; | 160 | return markedOptionsService; |
@@ -174,6 +177,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) | @@ -174,6 +177,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) | ||
174 | provide: MAT_DATE_LOCALE, | 177 | provide: MAT_DATE_LOCALE, |
175 | useValue: 'en-GB' | 178 | useValue: 'en-GB' |
176 | }, | 179 | }, |
180 | + { provide: HELP_MARKDOWN_COMPONENT_TOKEN, useValue: HelpMarkdownComponent }, | ||
181 | + { provide: SHARED_MODULE_TOKEN, useValue: SharedModule }, | ||
177 | TbPopoverService | 182 | TbPopoverService |
178 | ], | 183 | ], |
179 | declarations: [ | 184 | declarations: [ |
@@ -190,6 +195,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) | @@ -190,6 +195,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) | ||
190 | TbStringTemplateOutletDirective, | 195 | TbStringTemplateOutletDirective, |
191 | TbComponentOutletDirective, | 196 | TbComponentOutletDirective, |
192 | TbPopoverDirective, | 197 | TbPopoverDirective, |
198 | + TbMarkdownComponent, | ||
193 | HelpComponent, | 199 | HelpComponent, |
194 | HelpMarkdownComponent, | 200 | HelpMarkdownComponent, |
195 | HelpPopupComponent, | 201 | HelpPopupComponent, |
@@ -336,6 +342,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) | @@ -336,6 +342,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) | ||
336 | TbStringTemplateOutletDirective, | 342 | TbStringTemplateOutletDirective, |
337 | TbComponentOutletDirective, | 343 | TbComponentOutletDirective, |
338 | TbPopoverDirective, | 344 | TbPopoverDirective, |
345 | + TbMarkdownComponent, | ||
339 | HelpComponent, | 346 | HelpComponent, |
340 | HelpMarkdownComponent, | 347 | HelpMarkdownComponent, |
341 | HelpPopupComponent, | 348 | HelpPopupComponent, |
1 | +#### Custom action function | ||
2 | + | ||
3 | +<div class="divider"></div> | ||
4 | +<br/> | ||
5 | + | ||
6 | +*function ($event, widgetContext, entityId, entityName, additionalParams, entityLabel): void* | ||
7 | + | ||
8 | +A JavaScript function performing custom action. | ||
9 | + | ||
10 | +**Parameters:** | ||
11 | + | ||
12 | +<ul> | ||
13 | + <li><b>$event:</b> <code><a href="https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent" target="_blank">MouseEvent</a></code> - The <a href="https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent" target="_blank">MouseEvent</a> object. Usually a result of a mouse click event. | ||
14 | + </li> | ||
15 | + <li><b>widgetContext:</b> <code><a href="https://github.com/thingsboard/thingsboard/blob/5bb6403407aa4898084832d6698aa9ea6d484889/ui-ngx/src/app/modules/home/models/widget-component.models.ts#L107" target="_blank">WidgetContext</a></code> - A reference to <a href="https://github.com/thingsboard/thingsboard/blob/5bb6403407aa4898084832d6698aa9ea6d484889/ui-ngx/src/app/modules/home/models/widget-component.models.ts#L107" target="_blank">WidgetContext</a> that has all necessary API | ||
16 | + and data used by widget instance. | ||
17 | + </li> | ||
18 | + <li><b>entityId:</b> <code>string</code> - An optional string id of the target entity. | ||
19 | + </li> | ||
20 | + <li><b>entityName:</b> <code>string</code> - An optional string name of the target entity. | ||
21 | + </li> | ||
22 | + {% include widget/action/custom_additional_params %} | ||
23 | + <li><b>entityLabel:</b> <code>string</code> - An optional string label of the target entity. | ||
24 | + </li> | ||
25 | +</ul> | ||
26 | + | ||
27 | +<div class="divider"></div> | ||
28 | + | ||
29 | +##### Examples | ||
30 | + | ||
31 | +* Display alert dialog with entity information: | ||
32 | + | ||
33 | +```javascript | ||
34 | +var title; | ||
35 | +var content; | ||
36 | +if (entityName) { | ||
37 | + title = entityName + ' details'; | ||
38 | + content = '<b>Entity name</b>: ' + entityName; | ||
39 | + if (additionalParams && additionalParams.entity) { | ||
40 | + var entity = additionalParams.entity; | ||
41 | + if (entity.id) { | ||
42 | + content += '<br><b>Entity type</b>: ' + entity.id.entityType; | ||
43 | + } | ||
44 | + if (!isNaN(entity.temperature) && entity.temperature !== '') { | ||
45 | + content += '<br><b>Temperature</b>: ' + entity.temperature + ' °C'; | ||
46 | + } | ||
47 | + } | ||
48 | +} else { | ||
49 | + title = 'No entity information available'; | ||
50 | + content = '<b>No entity information available</b>'; | ||
51 | +} | ||
52 | + | ||
53 | +showAlertDialog(title, content); | ||
54 | + | ||
55 | +function showAlertDialog(title, content) { | ||
56 | + setTimeout(function() { | ||
57 | + widgetContext.dialogs.alert(title, content).subscribe(); | ||
58 | + }, 100); | ||
59 | +} | ||
60 | +{:copy-code} | ||
61 | +``` | ||
62 | + | ||
63 | +* Delete device after confirmation: | ||
64 | + | ||
65 | +```javascript | ||
66 | +var $injector = widgetContext.$scope.$injector; | ||
67 | +var dialogs = $injector.get(widgetContext.servicesMap.get('dialogs')); | ||
68 | +var deviceService = $injector.get(widgetContext.servicesMap.get('deviceService')); | ||
69 | + | ||
70 | +openDeleteDeviceDialog(); | ||
71 | + | ||
72 | +function openDeleteDeviceDialog() { | ||
73 | + var title = 'Are you sure you want to delete the device ' + entityName + '?'; | ||
74 | + var content = 'Be careful, after the confirmation, the device and all related data will become unrecoverable!'; | ||
75 | + dialogs.confirm(title, content, 'Cancel', 'Delete').subscribe( | ||
76 | + function(result) { | ||
77 | + if (result) { | ||
78 | + deleteDevice(); | ||
79 | + } | ||
80 | + } | ||
81 | + ); | ||
82 | +} | ||
83 | + | ||
84 | +function deleteDevice() { | ||
85 | + deviceService.deleteDevice(entityId.id).subscribe( | ||
86 | + function() { | ||
87 | + widgetContext.updateAliases(); | ||
88 | + } | ||
89 | + ); | ||
90 | +} | ||
91 | +{:copy-code} | ||
92 | +``` |
1 | + <li><b>additionalParams:</b> <code>{[key: string]: any}</code> - An optional key/value object holding additional entity parameters depending on widget type and action source: | ||
2 | + <ul> | ||
3 | + <li>Entities table widget (<i>On row click</i> or <i>Action cell button</i>) - <b>additionalParams:</b> <code>{ entity: <a href="https://github.com/thingsboard/thingsboard/blob/e264f7b8ddff05bda85c4833bf497f47f447496e/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts#L61" target="_blank">EntityData</a> }</code>: | ||
4 | + <ul> | ||
5 | + <li><b>entity:</b> <code><a href="https://github.com/thingsboard/thingsboard/blob/e264f7b8ddff05bda85c4833bf497f47f447496e/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts#L61" target="_blank">EntityData</a></code> - An | ||
6 | + <a href="https://github.com/thingsboard/thingsboard/blob/e264f7b8ddff05bda85c4833bf497f47f447496e/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts#L61" target="_blank">EntityData</a> object | ||
7 | + presenting basic entity properties (ex. <code>id</code>, <code>entityName</code>) and <br> provides access to other entity attributes/timeseries declared in widget datasource configuration. | ||
8 | + </li> | ||
9 | + </ul> | ||
10 | + </li> | ||
11 | + <li>Alarms table widget (<i>On row click</i> or <i>Action cell button</i>) - <b>additionalParams:</b> <code>{ alarm: <a href="https://github.com/thingsboard/thingsboard/blob/e264f7b8ddff05bda85c4833bf497f47f447496e/ui-ngx/src/app/shared/models/alarm.models.ts#L108" target="_blank">AlarmDataInfo</a> }</code>: | ||
12 | + <ul> | ||
13 | + <li><b>alarm:</b> <code><a href="https://github.com/thingsboard/thingsboard/blob/e264f7b8ddff05bda85c4833bf497f47f447496e/ui-ngx/src/app/shared/models/alarm.models.ts#L108" target="_blank">AlarmDataInfo</a></code> - An | ||
14 | + <a href="https://github.com/thingsboard/thingsboard/blob/e264f7b8ddff05bda85c4833bf497f47f447496e/ui-ngx/src/app/shared/models/alarm.models.ts#L108" target="_blank">AlarmDataInfo</a> object | ||
15 | + presenting basic alarm properties (ex. <code>type</code>, <code>severity</code>, <code>originator</code>, etc.) and <br> provides access to other alarm or originator entity fields/attributes/timeseries declared in widget datasource configuration. | ||
16 | + </li> | ||
17 | + </ul> | ||
18 | + </li> | ||
19 | + <li>Timeseries table widget (<i>On row click</i> or <i>Action cell button</i>) - <b>additionalParams:</b> <code><a href="https://github.com/thingsboard/thingsboard/blob/e264f7b8ddff05bda85c4833bf497f47f447496e/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts#L80" target="_blank">TimeseriesRow</a></code>: | ||
20 | + <ul> | ||
21 | + <li><b>additionalParams:</b> <code><a href="https://github.com/thingsboard/thingsboard/blob/e264f7b8ddff05bda85c4833bf497f47f447496e/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts#L80" target="_blank">TimeseriesRow</a></code> - A | ||
22 | + <a href="https://github.com/thingsboard/thingsboard/blob/e264f7b8ddff05bda85c4833bf497f47f447496e/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts#L80" target="_blank">TimeseriesRow</a> object | ||
23 | + presenting <code>formattedTs</code> (a string value of formatted timestamp) and <br> timeseries values for each column declared in widget datasource configuration. | ||
24 | + </li> | ||
25 | + </ul> | ||
26 | + </li> | ||
27 | + <li>Entities hierarchy widget (<i>On node selected</i>) - <b>additionalParams:</b> <code>{ nodeCtx: <a href="https://github.com/thingsboard/thingsboard/blob/e264f7b8ddff05bda85c4833bf497f47f447496e/ui-ngx/src/app/modules/home/components/widget/lib/entities-hierarchy-widget.models.ts#L35" target="_blank">HierarchyNodeContext</a> }</code>: | ||
28 | + <ul> | ||
29 | + <li><b>nodeCtx:</b> <code><a href="https://github.com/thingsboard/thingsboard/blob/e264f7b8ddff05bda85c4833bf497f47f447496e/ui-ngx/src/app/modules/home/components/widget/lib/entities-hierarchy-widget.models.ts#L35" target="_blank">HierarchyNodeContext</a></code> - An | ||
30 | + <a href="https://github.com/thingsboard/thingsboard/blob/e264f7b8ddff05bda85c4833bf497f47f447496e/ui-ngx/src/app/modules/home/components/widget/lib/entities-hierarchy-widget.models.ts#L35" target="_blank">HierarchyNodeContext</a> object | ||
31 | + containing <code>entity</code> field holding basic entity properties <br> (ex. <code>id</code>, <code>name</code>, <code>label</code>) and <code>data</code> field holding other entity attributes/timeseries declared in widget datasource configuration. | ||
32 | + </li> | ||
33 | + </ul> | ||
34 | + </li> | ||
35 | + <li>Pie - Flot widget (<i>On slice click</i>) - <b>additionalParams:</b> <code><a href="https://github.com/thingsboard/thingsboard/blob/e264f7b8ddff05bda85c4833bf497f47f447496e/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.models.ts#L62" target="_blank">TbFlotPlotItem</a></code>: | ||
36 | + <ul> | ||
37 | + <li><b>additionalParams:</b> <code><a href="https://github.com/thingsboard/thingsboard/blob/e264f7b8ddff05bda85c4833bf497f47f447496e/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.models.ts#L62" target="_blank">TbFlotPlotItem</a></code> - A | ||
38 | + <a href="https://github.com/thingsboard/thingsboard/blob/e264f7b8ddff05bda85c4833bf497f47f447496e/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.models.ts#L62" target="_blank">TbFlotPlotItem</a> object | ||
39 | + containing <code>series</code> field with information about datasource and <br> data key of clicked pie slice. | ||
40 | + </li> | ||
41 | + </ul> | ||
42 | + </li> | ||
43 | + <li><i>All other widgets</i> - does not provide <b>additionalParams</b> value. | ||
44 | + </li> | ||
45 | + </ul> | ||
46 | + </li> |
1 | +#### Custom action (with HTML template) function | ||
2 | + | ||
3 | +<div class="divider"></div> | ||
4 | +<br/> | ||
5 | + | ||
6 | +*function ($event, widgetContext, entityId, entityName, htmlTemplate, additionalParams, entityLabel): void* | ||
7 | + | ||
8 | +A JavaScript function performing custom action with defined HTML template to render dialog. | ||
9 | + | ||
10 | +**Parameters:** | ||
11 | + | ||
12 | +<ul> | ||
13 | + <li><b>$event:</b> <code><a href="https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent" target="_blank">MouseEvent</a></code> - The <a href="https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent" target="_blank">MouseEvent</a> object. Usually a result of a mouse click event. | ||
14 | + </li> | ||
15 | + <li><b>widgetContext:</b> <code><a href="https://github.com/thingsboard/thingsboard/blob/5bb6403407aa4898084832d6698aa9ea6d484889/ui-ngx/src/app/modules/home/models/widget-component.models.ts#L107" target="_blank">WidgetContext</a></code> - A reference to <a href="https://github.com/thingsboard/thingsboard/blob/5bb6403407aa4898084832d6698aa9ea6d484889/ui-ngx/src/app/modules/home/models/widget-component.models.ts#L107" target="_blank">WidgetContext</a> that has all necessary API | ||
16 | + and data used by widget instance. | ||
17 | + </li> | ||
18 | + <li><b>entityId:</b> <code>string</code> - An optional string id of the target entity. | ||
19 | + </li> | ||
20 | + <li><b>entityName:</b> <code>string</code> - An optional string name of the target entity. | ||
21 | + </li> | ||
22 | + <li><b>htmlTemplate:</b> <code>string</code> - An optional HTML template string defined in <b>HTML</b> tab.<br/> Used to render custom dialog (see <b>Examples</b> for more details). | ||
23 | + </li> | ||
24 | + {% include widget/action/custom_additional_params %} | ||
25 | + <li><b>entityLabel:</b> <code>string</code> - An optional string label of the target entity. | ||
26 | + </li> | ||
27 | +</ul> | ||
28 | + | ||
29 | +<div class="divider"></div> | ||
30 | + | ||
31 | +##### Examples | ||
32 | + | ||
33 | +###### Display dialog to create a device or an asset | ||
34 | + | ||
35 | +<br> | ||
36 | + | ||
37 | +<div style="padding-left: 64px;" | ||
38 | + tb-help-popup="widget/action/examples/custom_pretty_create_dialog_js" | ||
39 | + tb-help-popup-placement="top" | ||
40 | + [tb-help-popup-style]="{maxHeight: '50vh', maxWidth: '50vw'}" | ||
41 | + trigger-text="JavaScript function"> | ||
42 | +</div> | ||
43 | + | ||
44 | +<br> | ||
45 | + | ||
46 | +<div style="padding-left: 64px;" | ||
47 | + tb-help-popup="widget/action/examples/custom_pretty_create_dialog_html" | ||
48 | + tb-help-popup-placement="top" | ||
49 | + [tb-help-popup-style]="{maxHeight: '50vh', maxWidth: '50vw'}" | ||
50 | + trigger-text="HTML code"> | ||
51 | +</div> | ||
52 | + | ||
53 | +###### Display dialog to edit a device or an asset | ||
54 | + | ||
55 | +<br> | ||
56 | + | ||
57 | +<div style="padding-left: 64px;" | ||
58 | + tb-help-popup="widget/action/examples/custom_pretty_edit_dialog_js" | ||
59 | + tb-help-popup-placement="top" | ||
60 | + [tb-help-popup-style]="{maxHeight: '50vh', maxWidth: '50vw'}" | ||
61 | + trigger-text="JavaScript function"> | ||
62 | +</div> | ||
63 | + | ||
64 | +<br> | ||
65 | + | ||
66 | +<div style="padding-left: 64px;" | ||
67 | + tb-help-popup="widget/action/examples/custom_pretty_edit_dialog_html" | ||
68 | + tb-help-popup-placement="top" | ||
69 | + [tb-help-popup-style]="{maxHeight: '50vh', maxWidth: '50vw'}" | ||
70 | + trigger-text="HTML code"> | ||
71 | +</div> | ||
72 | + | ||
73 | +<br> | ||
74 | +<br> |
1 | +#### HTML template of dialog to create a device or an asset | ||
2 | + | ||
3 | +```html | ||
4 | +<form #addEntityForm="ngForm" [formGroup]="addEntityFormGroup" | ||
5 | + (ngSubmit)="save()" class="add-entity-form"> | ||
6 | + <mat-toolbar fxLayout="row" color="primary"> | ||
7 | + <h2>Add entity</h2> | ||
8 | + <span fxFlex></span> | ||
9 | + <button mat-icon-button (click)="cancel()" type="button"> | ||
10 | + <mat-icon class="material-icons">close</mat-icon> | ||
11 | + </button> | ||
12 | + </mat-toolbar> | ||
13 | + <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> | ||
14 | + </mat-progress-bar> | ||
15 | + <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div> | ||
16 | + <div mat-dialog-content fxLayout="column"> | ||
17 | + <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0"> | ||
18 | + <mat-form-field fxFlex class="mat-block"> | ||
19 | + <mat-label>Entity Name</mat-label> | ||
20 | + <input matInput formControlName="entityName" required> | ||
21 | + <mat-error *ngIf="addEntityFormGroup.get('entityName').hasError('required')"> | ||
22 | + Entity name is required. | ||
23 | + </mat-error> | ||
24 | + </mat-form-field> | ||
25 | + <mat-form-field fxFlex class="mat-block"> | ||
26 | + <mat-label>Entity Label</mat-label> | ||
27 | + <input matInput formControlName="entityLabel" > | ||
28 | + </mat-form-field> | ||
29 | + </div> | ||
30 | + <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0"> | ||
31 | + <tb-entity-type-select | ||
32 | + class="mat-block" | ||
33 | + formControlName="entityType" | ||
34 | + [showLabel]="true" | ||
35 | + [allowedEntityTypes]="allowedEntityTypes" | ||
36 | + ></tb-entity-type-select> | ||
37 | + <tb-entity-subtype-autocomplete | ||
38 | + fxFlex *ngIf="addEntityFormGroup.get('entityType').value == 'ASSET'" | ||
39 | + class="mat-block" | ||
40 | + formControlName="type" | ||
41 | + [required]="true" | ||
42 | + [entityType]="'ASSET'" | ||
43 | + ></tb-entity-subtype-autocomplete> | ||
44 | + <tb-entity-subtype-autocomplete | ||
45 | + fxFlex *ngIf="addEntityFormGroup.get('entityType').value != 'ASSET'" | ||
46 | + class="mat-block" | ||
47 | + formControlName="type" | ||
48 | + [required]="true" | ||
49 | + [entityType]="'DEVICE'" | ||
50 | + ></tb-entity-subtype-autocomplete> | ||
51 | + </div> | ||
52 | + <div formGroupName="attributes" fxLayout="column"> | ||
53 | + <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0"> | ||
54 | + <mat-form-field fxFlex class="mat-block"> | ||
55 | + <mat-label>Latitude</mat-label> | ||
56 | + <input type="number" step="any" matInput formControlName="latitude"> | ||
57 | + </mat-form-field> | ||
58 | + <mat-form-field fxFlex class="mat-block"> | ||
59 | + <mat-label>Longitude</mat-label> | ||
60 | + <input type="number" step="any" matInput formControlName="longitude"> | ||
61 | + </mat-form-field> | ||
62 | + </div> | ||
63 | + <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0"> | ||
64 | + <mat-form-field fxFlex class="mat-block"> | ||
65 | + <mat-label>Address</mat-label> | ||
66 | + <input matInput formControlName="address"> | ||
67 | + </mat-form-field> | ||
68 | + <mat-form-field fxFlex class="mat-block"> | ||
69 | + <mat-label>Owner</mat-label> | ||
70 | + <input matInput formControlName="owner"> | ||
71 | + </mat-form-field> | ||
72 | + </div> | ||
73 | + <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0"> | ||
74 | + <mat-form-field fxFlex class="mat-block"> | ||
75 | + <mat-label>Integer Value</mat-label> | ||
76 | + <input type="number" step="1" matInput formControlName="number"> | ||
77 | + <mat-error *ngIf="addEntityFormGroup.get('attributes.number').hasError('pattern')"> | ||
78 | + Invalid integer value. | ||
79 | + </mat-error> | ||
80 | + </mat-form-field> | ||
81 | + <div class="boolean-value-input" fxLayout="column" fxLayoutAlign="center start" fxFlex> | ||
82 | + <label class="checkbox-label">Boolean Value</label> | ||
83 | + <mat-checkbox formControlName="booleanValue" style="margin-bottom: 40px;"> | ||
84 | + | ||
85 | + </mat-checkbox> | ||
86 | + </div> | ||
87 | + </div> | ||
88 | + </div> | ||
89 | + <div class="relations-list"> | ||
90 | + <div class="mat-body-1" style="padding-bottom: 10px; color: rgba(0,0,0,0.57);">Relations</div> | ||
91 | + <div class="body" [fxShow]="relations().length"> | ||
92 | + <div class="row" fxLayout="row" fxLayoutAlign="start center" formArrayName="relations" *ngFor="let relation of relations().controls; let i = index;"> | ||
93 | + <div [formGroupName]="i" class="mat-elevation-z2" fxFlex fxLayout="row" style="padding: 5px 0 5px 5px;"> | ||
94 | + <div fxFlex fxLayout="column"> | ||
95 | + <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0"> | ||
96 | + <mat-form-field class="mat-block" style="min-width: 100px;"> | ||
97 | + <mat-label>Direction</mat-label> | ||
98 | + <mat-select formControlName="direction" name="direction"> | ||
99 | + <mat-option *ngFor="let direction of entitySearchDirection | keyvalue" [value]="direction.value"> | ||
100 | + | ||
101 | + </mat-option> | ||
102 | + </mat-select> | ||
103 | + <mat-error *ngIf="relation.get('direction').hasError('required')"> | ||
104 | + Relation direction is required. | ||
105 | + </mat-error> | ||
106 | + </mat-form-field> | ||
107 | + <tb-relation-type-autocomplete | ||
108 | + fxFlex class="mat-block" | ||
109 | + formControlName="relationType" | ||
110 | + [required]="true"> | ||
111 | + </tb-relation-type-autocomplete> | ||
112 | + </div> | ||
113 | + <div fxLayout="row" fxLayout.xs="column"> | ||
114 | + <tb-entity-select | ||
115 | + fxFlex class="mat-block" | ||
116 | + [required]="true" | ||
117 | + formControlName="relatedEntity"> | ||
118 | + </tb-entity-select> | ||
119 | + </div> | ||
120 | + </div> | ||
121 | + <div fxLayout="column" fxLayoutAlign="center center"> | ||
122 | + <button mat-icon-button color="primary" | ||
123 | + aria-label="Remove" | ||
124 | + type="button" | ||
125 | + (click)="removeRelation(i)" | ||
126 | + matTooltip="Remove relation" | ||
127 | + matTooltipPosition="above"> | ||
128 | + <mat-icon>close</mat-icon> | ||
129 | + </button> | ||
130 | + </div> | ||
131 | + </div> | ||
132 | + </div> | ||
133 | + </div> | ||
134 | + <div> | ||
135 | + <button mat-raised-button color="primary" | ||
136 | + type="button" | ||
137 | + (click)="addRelation()" | ||
138 | + matTooltip="Add Relation" | ||
139 | + matTooltipPosition="above"> | ||
140 | + Add | ||
141 | + </button> | ||
142 | + </div> | ||
143 | + </div> | ||
144 | + </div> | ||
145 | + <div mat-dialog-actions fxLayout="row" fxLayoutAlign="end center"> | ||
146 | + <button mat-button color="primary" | ||
147 | + type="button" | ||
148 | + [disabled]="(isLoading$ | async)" | ||
149 | + (click)="cancel()" cdkFocusInitial> | ||
150 | + Cancel | ||
151 | + </button> | ||
152 | + <button mat-button mat-raised-button color="primary" | ||
153 | + type="submit" | ||
154 | + [disabled]="(isLoading$ | async) || addEntityForm.invalid || !addEntityForm.dirty"> | ||
155 | + Create | ||
156 | + </button> | ||
157 | + </div> | ||
158 | +</form> | ||
159 | +{:copy-code} | ||
160 | +``` |
1 | +#### Function displaying dialog to create a device or an asset | ||
2 | + | ||
3 | +```javascript | ||
4 | +let $injector = widgetContext.$scope.$injector; | ||
5 | +let customDialog = $injector.get(widgetContext.servicesMap.get('customDialog')); | ||
6 | +let assetService = $injector.get(widgetContext.servicesMap.get('assetService')); | ||
7 | +let deviceService = $injector.get(widgetContext.servicesMap.get('deviceService')); | ||
8 | +let attributeService = $injector.get(widgetContext.servicesMap.get('attributeService')); | ||
9 | +let entityRelationService = $injector.get(widgetContext.servicesMap.get('entityRelationService')); | ||
10 | + | ||
11 | +openAddEntityDialog(); | ||
12 | + | ||
13 | +function openAddEntityDialog() { | ||
14 | + customDialog.customDialog(htmlTemplate, AddEntityDialogController).subscribe(); | ||
15 | +} | ||
16 | + | ||
17 | +function AddEntityDialogController(instance) { | ||
18 | + let vm = instance; | ||
19 | + | ||
20 | + vm.allowedEntityTypes = ['ASSET', 'DEVICE']; | ||
21 | + vm.entitySearchDirection = { | ||
22 | + from: "FROM", | ||
23 | + to: "TO" | ||
24 | + } | ||
25 | + | ||
26 | + vm.addEntityFormGroup = vm.fb.group({ | ||
27 | + entityName: ['', [vm.validators.required]], | ||
28 | + entityType: ['DEVICE'], | ||
29 | + entityLabel: [null], | ||
30 | + type: ['', [vm.validators.required]], | ||
31 | + attributes: vm.fb.group({ | ||
32 | + latitude: [null], | ||
33 | + longitude: [null], | ||
34 | + address: [null], | ||
35 | + owner: [null], | ||
36 | + number: [null, [vm.validators.pattern(/^-?[0-9]+$/)]], | ||
37 | + booleanValue: [null] | ||
38 | + }), | ||
39 | + relations: vm.fb.array([]) | ||
40 | + }); | ||
41 | + | ||
42 | + vm.cancel = function () { | ||
43 | + vm.dialogRef.close(null); | ||
44 | + }; | ||
45 | + | ||
46 | + vm.relations = function () { | ||
47 | + return vm.addEntityFormGroup.get('relations'); | ||
48 | + }; | ||
49 | + | ||
50 | + vm.addRelation = function () { | ||
51 | + vm.relations().push(vm.fb.group({ | ||
52 | + relatedEntity: [null, [vm.validators.required]], | ||
53 | + relationType: [null, [vm.validators.required]], | ||
54 | + direction: [null, [vm.validators.required]] | ||
55 | + })); | ||
56 | + }; | ||
57 | + | ||
58 | + vm.removeRelation = function (index) { | ||
59 | + vm.relations().removeAt(index); | ||
60 | + vm.relations().markAsDirty(); | ||
61 | + }; | ||
62 | + | ||
63 | + vm.save = function () { | ||
64 | + vm.addEntityFormGroup.markAsPristine(); | ||
65 | + saveEntityObservable().subscribe( | ||
66 | + function (entity) { | ||
67 | + widgetContext.rxjs.forkJoin([ | ||
68 | + saveAttributes(entity.id), | ||
69 | + saveRelations(entity.id) | ||
70 | + ]).subscribe( | ||
71 | + function () { | ||
72 | + widgetContext.updateAliases(); | ||
73 | + vm.dialogRef.close(null); | ||
74 | + } | ||
75 | + ); | ||
76 | + } | ||
77 | + ); | ||
78 | + }; | ||
79 | + | ||
80 | + function saveEntityObservable() { | ||
81 | + const formValues = vm.addEntityFormGroup.value; | ||
82 | + let entity = { | ||
83 | + name: formValues.entityName, | ||
84 | + type: formValues.type, | ||
85 | + label: formValues.entityLabel | ||
86 | + }; | ||
87 | + if (formValues.entityType == 'ASSET') { | ||
88 | + return assetService.saveAsset(entity); | ||
89 | + } else if (formValues.entityType == 'DEVICE') { | ||
90 | + return deviceService.saveDevice(entity); | ||
91 | + } | ||
92 | + } | ||
93 | + | ||
94 | + function saveAttributes(entityId) { | ||
95 | + let attributes = vm.addEntityFormGroup.get('attributes').value; | ||
96 | + let attributesArray = []; | ||
97 | + for (let key in attributes) { | ||
98 | + if (attributes[key] !== null) { | ||
99 | + attributesArray.push({key: key, value: attributes[key]}); | ||
100 | + } | ||
101 | + } | ||
102 | + if (attributesArray.length > 0) { | ||
103 | + return attributeService.saveEntityAttributes(entityId, "SERVER_SCOPE", attributesArray); | ||
104 | + } | ||
105 | + return widgetContext.rxjs.of([]); | ||
106 | + } | ||
107 | + | ||
108 | + function saveRelations(entityId) { | ||
109 | + let relations = vm.addEntityFormGroup.get('relations').value; | ||
110 | + let tasks = []; | ||
111 | + for (let i = 0; i < relations.length; i++) { | ||
112 | + let relation = { | ||
113 | + type: relations[i].relationType, | ||
114 | + typeGroup: 'COMMON' | ||
115 | + }; | ||
116 | + if (relations[i].direction == 'FROM') { | ||
117 | + relation.to = relations[i].relatedEntity; | ||
118 | + relation.from = entityId; | ||
119 | + } else { | ||
120 | + relation.to = entityId; | ||
121 | + relation.from = relations[i].relatedEntity; | ||
122 | + } | ||
123 | + tasks.push(entityRelationService.saveRelation(relation)); | ||
124 | + } | ||
125 | + if (tasks.length > 0) { | ||
126 | + return widgetContext.rxjs.forkJoin(tasks); | ||
127 | + } | ||
128 | + return widgetContext.rxjs.of([]); | ||
129 | + } | ||
130 | +} | ||
131 | +{:copy-code} | ||
132 | +``` |
1 | +#### HTML template of dialog to edit a device or an asset | ||
2 | + | ||
3 | +```html | ||
4 | +<form #editEntityForm="ngForm" [formGroup]="editEntityFormGroup" | ||
5 | + (ngSubmit)="save()" class="edit-entity-form"> | ||
6 | + <mat-toolbar fxLayout="row" color="primary"> | ||
7 | + <h2>Edit </h2> | ||
8 | + <span fxFlex></span> | ||
9 | + <button mat-icon-button (click)="cancel()" type="button"> | ||
10 | + <mat-icon class="material-icons">close</mat-icon> | ||
11 | + </button> | ||
12 | + </mat-toolbar> | ||
13 | + <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> | ||
14 | + </mat-progress-bar> | ||
15 | + <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div> | ||
16 | + <div mat-dialog-content fxLayout="column"> | ||
17 | + <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0"> | ||
18 | + <mat-form-field fxFlex class="mat-block"> | ||
19 | + <mat-label>Entity Name</mat-label> | ||
20 | + <input matInput formControlName="entityName" required readonly=""> | ||
21 | + </mat-form-field> | ||
22 | + <mat-form-field fxFlex class="mat-block"> | ||
23 | + <mat-label>Entity Label</mat-label> | ||
24 | + <input matInput formControlName="entityLabel"> | ||
25 | + </mat-form-field> | ||
26 | + </div> | ||
27 | + <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0"> | ||
28 | + <mat-form-field fxFlex class="mat-block"> | ||
29 | + <mat-label>Entity Type</mat-label> | ||
30 | + <input matInput formControlName="entityType" readonly> | ||
31 | + </mat-form-field> | ||
32 | + <mat-form-field fxFlex class="mat-block"> | ||
33 | + <mat-label>Type</mat-label> | ||
34 | + <input matInput formControlName="type" readonly> | ||
35 | + </mat-form-field> | ||
36 | + </div> | ||
37 | + <div formGroupName="attributes" fxLayout="column"> | ||
38 | + <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0"> | ||
39 | + <mat-form-field fxFlex class="mat-block"> | ||
40 | + <mat-label>Latitude</mat-label> | ||
41 | + <input type="number" step="any" matInput formControlName="latitude"> | ||
42 | + </mat-form-field> | ||
43 | + <mat-form-field fxFlex class="mat-block"> | ||
44 | + <mat-label>Longitude</mat-label> | ||
45 | + <input type="number" step="any" matInput formControlName="longitude"> | ||
46 | + </mat-form-field> | ||
47 | + </div> | ||
48 | + <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0"> | ||
49 | + <mat-form-field fxFlex class="mat-block"> | ||
50 | + <mat-label>Address</mat-label> | ||
51 | + <input matInput formControlName="address"> | ||
52 | + </mat-form-field> | ||
53 | + <mat-form-field fxFlex class="mat-block"> | ||
54 | + <mat-label>Owner</mat-label> | ||
55 | + <input matInput formControlName="owner"> | ||
56 | + </mat-form-field> | ||
57 | + </div> | ||
58 | + <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0"> | ||
59 | + <mat-form-field fxFlex class="mat-block"> | ||
60 | + <mat-label>Integer Value</mat-label> | ||
61 | + <input type="number" step="1" matInput formControlName="number"> | ||
62 | + <mat-error *ngIf="editEntityFormGroup.get('attributes.number').hasError('pattern')"> | ||
63 | + Invalid integer value. | ||
64 | + </mat-error> | ||
65 | + </mat-form-field> | ||
66 | + <div class="boolean-value-input" fxLayout="column" fxLayoutAlign="center start" fxFlex> | ||
67 | + <label class="checkbox-label">Boolean Value</label> | ||
68 | + <mat-checkbox formControlName="booleanValue" style="margin-bottom: 40px;"> | ||
69 | + | ||
70 | + </mat-checkbox> | ||
71 | + </div> | ||
72 | + </div> | ||
73 | + </div> | ||
74 | + <div class="relations-list old-relations"> | ||
75 | + <div class="mat-body-1" style="padding-bottom: 10px; color: rgba(0,0,0,0.57);">Relations</div> | ||
76 | + <div class="body" [fxShow]="oldRelations().length"> | ||
77 | + <div class="row" fxLayout="row" fxLayoutAlign="start center" formArrayName="oldRelations" | ||
78 | + *ngFor="let relation of oldRelations().controls; let i = index;"> | ||
79 | + <div [formGroupName]="i" class="mat-elevation-z2" fxFlex fxLayout="row" style="padding: 5px 0 5px 5px;"> | ||
80 | + <div fxFlex fxLayout="column"> | ||
81 | + <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0"> | ||
82 | + <mat-form-field class="mat-block" style="min-width: 100px;"> | ||
83 | + <mat-label>Direction</mat-label> | ||
84 | + <mat-select formControlName="direction" name="direction"> | ||
85 | + <mat-option *ngFor="let direction of entitySearchDirection | keyvalue" [value]="direction.value"> | ||
86 | + | ||
87 | + </mat-option> | ||
88 | + </mat-select> | ||
89 | + <mat-error *ngIf="relation.get('direction').hasError('required')"> | ||
90 | + Relation direction is required. | ||
91 | + </mat-error> | ||
92 | + </mat-form-field> | ||
93 | + <tb-relation-type-autocomplete | ||
94 | + fxFlex class="mat-block" | ||
95 | + formControlName="relationType" | ||
96 | + required="true"> | ||
97 | + </tb-relation-type-autocomplete> | ||
98 | + </div> | ||
99 | + <div fxLayout="row" fxLayout.xs="column"> | ||
100 | + <tb-entity-select | ||
101 | + fxFlex class="mat-block" | ||
102 | + required="true" | ||
103 | + formControlName="relatedEntity"> | ||
104 | + </tb-entity-select> | ||
105 | + </div> | ||
106 | + </div> | ||
107 | + <div fxLayout="column" fxLayoutAlign="center center"> | ||
108 | + <button mat-icon-button color="primary" | ||
109 | + aria-label="Remove" | ||
110 | + type="button" | ||
111 | + (click)="removeOldRelation(i)" | ||
112 | + matTooltip="Remove relation" | ||
113 | + matTooltipPosition="above"> | ||
114 | + <mat-icon>close</mat-icon> | ||
115 | + </button> | ||
116 | + </div> | ||
117 | + </div> | ||
118 | + </div> | ||
119 | + </div> | ||
120 | + </div> | ||
121 | + <div class="relations-list"> | ||
122 | + <div class="mat-body-1" style="padding-bottom: 10px; color: rgba(0,0,0,0.57);">New Relations</div> | ||
123 | + <div class="body" [fxShow]="relations().length"> | ||
124 | + <div class="row" fxLayout="row" fxLayoutAlign="start center" formArrayName="relations" *ngFor="let relation of relations().controls; let i = index;"> | ||
125 | + <div [formGroupName]="i" class="mat-elevation-z2" fxFlex fxLayout="row" style="padding: 5px 0 5px 5px;"> | ||
126 | + <div fxFlex fxLayout="column"> | ||
127 | + <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column" fxLayoutGap.xs="0"> | ||
128 | + <mat-form-field class="mat-block" style="min-width: 100px;"> | ||
129 | + <mat-label>Direction</mat-label> | ||
130 | + <mat-select formControlName="direction" name="direction"> | ||
131 | + <mat-option *ngFor="let direction of entitySearchDirection | keyvalue" [value]="direction.value"> | ||
132 | + | ||
133 | + </mat-option> | ||
134 | + </mat-select> | ||
135 | + <mat-error *ngIf="relation.get('direction').hasError('required')"> | ||
136 | + Relation direction is required. | ||
137 | + </mat-error> | ||
138 | + </mat-form-field> | ||
139 | + <tb-relation-type-autocomplete | ||
140 | + fxFlex class="mat-block" | ||
141 | + formControlName="relationType" | ||
142 | + [required]="true"> | ||
143 | + </tb-relation-type-autocomplete> | ||
144 | + </div> | ||
145 | + <div fxLayout="row" fxLayout.xs="column"> | ||
146 | + <tb-entity-select | ||
147 | + fxFlex class="mat-block" | ||
148 | + [required]="true" | ||
149 | + formControlName="relatedEntity"> | ||
150 | + </tb-entity-select> | ||
151 | + </div> | ||
152 | + </div> | ||
153 | + <div fxLayout="column" fxLayoutAlign="center center"> | ||
154 | + <button mat-icon-button color="primary" | ||
155 | + aria-label="Remove" | ||
156 | + type="button" | ||
157 | + (click)="removeRelation(i)" | ||
158 | + matTooltip="Remove relation" | ||
159 | + matTooltipPosition="above"> | ||
160 | + <mat-icon>close</mat-icon> | ||
161 | + </button> | ||
162 | + </div> | ||
163 | + </div> | ||
164 | + </div> | ||
165 | + </div> | ||
166 | + <div> | ||
167 | + <button mat-raised-button color="primary" | ||
168 | + type="button" | ||
169 | + (click)="addRelation()" | ||
170 | + matTooltip="Add Relation" | ||
171 | + matTooltipPosition="above"> | ||
172 | + Add | ||
173 | + </button> | ||
174 | + </div> | ||
175 | + </div> | ||
176 | + </div> | ||
177 | + <div mat-dialog-actions fxLayout="row" fxLayoutAlign="end center"> | ||
178 | + <button mat-button color="primary" | ||
179 | + type="button" | ||
180 | + [disabled]="(isLoading$ | async)" | ||
181 | + (click)="cancel()" cdkFocusInitial> | ||
182 | + Cancel | ||
183 | + </button> | ||
184 | + <button mat-button mat-raised-button color="primary" | ||
185 | + type="submit" | ||
186 | + [disabled]="(isLoading$ | async) || editEntityForm.invalid || !editEntityForm.dirty"> | ||
187 | + Save | ||
188 | + </button> | ||
189 | + </div> | ||
190 | +</form> | ||
191 | +{:copy-code} | ||
192 | +``` |
1 | +#### Function displaying dialog to edit a device or an asset | ||
2 | + | ||
3 | +```javascript | ||
4 | +let $injector = widgetContext.$scope.$injector; | ||
5 | +let customDialog = $injector.get(widgetContext.servicesMap.get('customDialog')); | ||
6 | +let entityService = $injector.get(widgetContext.servicesMap.get('entityService')); | ||
7 | +let assetService = $injector.get(widgetContext.servicesMap.get('assetService')); | ||
8 | +let deviceService = $injector.get(widgetContext.servicesMap.get('deviceService')); | ||
9 | +let attributeService = $injector.get(widgetContext.servicesMap.get('attributeService')); | ||
10 | +let entityRelationService = $injector.get(widgetContext.servicesMap.get('entityRelationService')); | ||
11 | + | ||
12 | +openEditEntityDialog(); | ||
13 | + | ||
14 | +function openEditEntityDialog() { | ||
15 | + customDialog.customDialog(htmlTemplate, EditEntityDialogController).subscribe(); | ||
16 | +} | ||
17 | + | ||
18 | +function EditEntityDialogController(instance) { | ||
19 | + let vm = instance; | ||
20 | + | ||
21 | + vm.entityName = entityName; | ||
22 | + vm.entityType = entityId.entityType; | ||
23 | + vm.entitySearchDirection = { | ||
24 | + from: "FROM", | ||
25 | + to: "TO" | ||
26 | + }; | ||
27 | + vm.attributes = {}; | ||
28 | + vm.oldRelationsData = []; | ||
29 | + vm.relationsToDelete = []; | ||
30 | + vm.entity = {}; | ||
31 | + | ||
32 | + vm.editEntityFormGroup = vm.fb.group({ | ||
33 | + entityName: ['', [vm.validators.required]], | ||
34 | + entityType: [null], | ||
35 | + entityLabel: [null], | ||
36 | + type: ['', [vm.validators.required]], | ||
37 | + attributes: vm.fb.group({ | ||
38 | + latitude: [null], | ||
39 | + longitude: [null], | ||
40 | + address: [null], | ||
41 | + owner: [null], | ||
42 | + number: [null, [vm.validators.pattern(/^-?[0-9]+$/)]], | ||
43 | + booleanValue: [false] | ||
44 | + }), | ||
45 | + oldRelations: vm.fb.array([]), | ||
46 | + relations: vm.fb.array([]) | ||
47 | + }); | ||
48 | + | ||
49 | + getEntityInfo(); | ||
50 | + | ||
51 | + vm.cancel = function() { | ||
52 | + vm.dialogRef.close(null); | ||
53 | + }; | ||
54 | + | ||
55 | + vm.relations = function() { | ||
56 | + return vm.editEntityFormGroup.get('relations'); | ||
57 | + }; | ||
58 | + | ||
59 | + vm.oldRelations = function() { | ||
60 | + return vm.editEntityFormGroup.get('oldRelations'); | ||
61 | + }; | ||
62 | + | ||
63 | + vm.addRelation = function() { | ||
64 | + vm.relations().push(vm.fb.group({ | ||
65 | + relatedEntity: [null, [vm.validators.required]], | ||
66 | + relationType: [null, [vm.validators.required]], | ||
67 | + direction: [null, [vm.validators.required]] | ||
68 | + })); | ||
69 | + }; | ||
70 | + | ||
71 | + function addOldRelation() { | ||
72 | + vm.oldRelations().push(vm.fb.group({ | ||
73 | + relatedEntity: [{value: null, disabled: true}, [vm.validators.required]], | ||
74 | + relationType: [{value: null, disabled: true}, [vm.validators.required]], | ||
75 | + direction: [{value: null, disabled: true}, [vm.validators.required]] | ||
76 | + })); | ||
77 | + } | ||
78 | + | ||
79 | + vm.removeRelation = function(index) { | ||
80 | + vm.relations().removeAt(index); | ||
81 | + vm.relations().markAsDirty(); | ||
82 | + }; | ||
83 | + | ||
84 | + vm.removeOldRelation = function(index) { | ||
85 | + vm.oldRelations().removeAt(index); | ||
86 | + vm.relationsToDelete.push(vm.oldRelationsData[index]); | ||
87 | + vm.oldRelations().markAsDirty(); | ||
88 | + }; | ||
89 | + | ||
90 | + vm.save = function() { | ||
91 | + vm.editEntityFormGroup.markAsPristine(); | ||
92 | + widgetContext.rxjs.forkJoin([ | ||
93 | + saveAttributes(entityId), | ||
94 | + saveRelations(entityId), | ||
95 | + saveEntity() | ||
96 | + ]).subscribe( | ||
97 | + function () { | ||
98 | + widgetContext.updateAliases(); | ||
99 | + vm.dialogRef.close(null); | ||
100 | + } | ||
101 | + ); | ||
102 | + }; | ||
103 | + | ||
104 | + function getEntityAttributes(attributes) { | ||
105 | + for (var i = 0; i < attributes.length; i++) { | ||
106 | + vm.attributes[attributes[i].key] = attributes[i].value; | ||
107 | + } | ||
108 | + } | ||
109 | + | ||
110 | + function getEntityRelations(relations) { | ||
111 | + let relationsFrom = relations[0]; | ||
112 | + let relationsTo = relations[1]; | ||
113 | + for (let i=0; i < relationsFrom.length; i++) { | ||
114 | + let relation = { | ||
115 | + direction: 'FROM', | ||
116 | + relationType: relationsFrom[i].type, | ||
117 | + relatedEntity: relationsFrom[i].to | ||
118 | + }; | ||
119 | + vm.oldRelationsData.push(relation); | ||
120 | + addOldRelation(); | ||
121 | + } | ||
122 | + for (let i=0; i < relationsTo.length; i++) { | ||
123 | + let relation = { | ||
124 | + direction: 'TO', | ||
125 | + relationType: relationsTo[i].type, | ||
126 | + relatedEntity: relationsTo[i].from | ||
127 | + }; | ||
128 | + vm.oldRelationsData.push(relation); | ||
129 | + addOldRelation(); | ||
130 | + } | ||
131 | + } | ||
132 | + | ||
133 | + function getEntityInfo() { | ||
134 | + widgetContext.rxjs.forkJoin([ | ||
135 | + entityRelationService.findInfoByFrom(entityId), | ||
136 | + entityRelationService.findInfoByTo(entityId), | ||
137 | + attributeService.getEntityAttributes(entityId, 'SERVER_SCOPE'), | ||
138 | + entityService.getEntity(entityId.entityType, entityId.id) | ||
139 | + ]).subscribe( | ||
140 | + function (data) { | ||
141 | + getEntityRelations(data.slice(0,2)); | ||
142 | + getEntityAttributes(data[2]); | ||
143 | + vm.entity = data[3]; | ||
144 | + vm.editEntityFormGroup.patchValue({ | ||
145 | + entityName: vm.entity.name, | ||
146 | + entityType: vm.entityType, | ||
147 | + entityLabel: vm.entity.label, | ||
148 | + type: vm.entity.type, | ||
149 | + attributes: vm.attributes, | ||
150 | + oldRelations: vm.oldRelationsData | ||
151 | + }, {emitEvent: false}); | ||
152 | + } | ||
153 | + ); | ||
154 | + } | ||
155 | + | ||
156 | + function saveEntity() { | ||
157 | + const formValues = vm.editEntityFormGroup.value; | ||
158 | + if (vm.entity.label !== formValues.entityLabel){ | ||
159 | + vm.entity.label = formValues.entityLabel; | ||
160 | + if (formValues.entityType == 'ASSET') { | ||
161 | + return assetService.saveAsset(vm.entity); | ||
162 | + } else if (formValues.entityType == 'DEVICE') { | ||
163 | + return deviceService.saveDevice(vm.entity); | ||
164 | + } | ||
165 | + } | ||
166 | + return widgetContext.rxjs.of([]); | ||
167 | + } | ||
168 | + | ||
169 | + function saveAttributes(entityId) { | ||
170 | + let attributes = vm.editEntityFormGroup.get('attributes').value; | ||
171 | + let attributesArray = []; | ||
172 | + for (let key in attributes) { | ||
173 | + if (attributes[key] !== vm.attributes[key]) { | ||
174 | + attributesArray.push({key: key, value: attributes[key]}); | ||
175 | + } | ||
176 | + } | ||
177 | + if (attributesArray.length > 0) { | ||
178 | + return attributeService.saveEntityAttributes(entityId, "SERVER_SCOPE", attributesArray); | ||
179 | + } | ||
180 | + return widgetContext.rxjs.of([]); | ||
181 | + } | ||
182 | + | ||
183 | + function saveRelations(entityId) { | ||
184 | + let relations = vm.editEntityFormGroup.get('relations').value; | ||
185 | + let tasks = []; | ||
186 | + for(let i=0; i < relations.length; i++) { | ||
187 | + let relation = { | ||
188 | + type: relations[i].relationType, | ||
189 | + typeGroup: 'COMMON' | ||
190 | + }; | ||
191 | + if (relations[i].direction == 'FROM') { | ||
192 | + relation.to = relations[i].relatedEntity; | ||
193 | + relation.from = entityId; | ||
194 | + } else { | ||
195 | + relation.to = entityId; | ||
196 | + relation.from = relations[i].relatedEntity; | ||
197 | + } | ||
198 | + tasks.push(entityRelationService.saveRelation(relation)); | ||
199 | + } | ||
200 | + for (let i=0; i < vm.relationsToDelete.length; i++) { | ||
201 | + let relation = { | ||
202 | + type: vm.relationsToDelete[i].relationType | ||
203 | + }; | ||
204 | + if (vm.relationsToDelete[i].direction == 'FROM') { | ||
205 | + relation.to = vm.relationsToDelete[i].relatedEntity; | ||
206 | + relation.from = entityId; | ||
207 | + } else { | ||
208 | + relation.to = entityId; | ||
209 | + relation.from = vm.relationsToDelete[i].relatedEntity; | ||
210 | + } | ||
211 | + tasks.push(entityRelationService.deleteRelation(relation.from, relation.type, relation.to)); | ||
212 | + } | ||
213 | + if (tasks.length > 0) { | ||
214 | + return widgetContext.rxjs.forkJoin(tasks); | ||
215 | + } | ||
216 | + return widgetContext.rxjs.of([]); | ||
217 | + } | ||
218 | +} | ||
219 | +{:copy-code} | ||
220 | +``` |
1 | -#### Show cell button action JavaScript Function | 1 | +#### Show cell button action function |
2 | + | ||
3 | +<div class="divider"></div> | ||
4 | +<br/> | ||
5 | + | ||
6 | +*function (widgetContext, data): boolean* | ||
7 | + | ||
8 | +A JavaScript function evaluating whether to display particular table cell action. | ||
9 | + | ||
10 | +**Parameters:** | ||
11 | + | ||
12 | +<ul> | ||
13 | + <li><b>widgetContext:</b> <code><a href="https://github.com/thingsboard/thingsboard/blob/5bb6403407aa4898084832d6698aa9ea6d484889/ui-ngx/src/app/modules/home/models/widget-component.models.ts#L107" target="_blank">WidgetContext</a></code> - A reference to <a href="https://github.com/thingsboard/thingsboard/blob/5bb6403407aa4898084832d6698aa9ea6d484889/ui-ngx/src/app/modules/home/models/widget-component.models.ts#L107" target="_blank">WidgetContext</a> that has all necessary API | ||
14 | + and data used by widget instance. | ||
15 | + </li> | ||
16 | + <li><b>data:</b> <code><a href="https://github.com/thingsboard/thingsboard/blob/5bb6403407aa4898084832d6698aa9ea6d484889/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts#L108" target="_blank">FormattedData</a></code> - A <a href="https://github.com/thingsboard/thingsboard/blob/5bb6403407aa4898084832d6698aa9ea6d484889/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts#L108" target="_blank">FormattedData</a> object of specific table row.<br/> | ||
17 | + Represents basic entity properties (ex. <code>entityId</code>, <code>entityName</code>)<br/>and provides access to other entity attributes/timeseries declared in widget datasource configuration. | ||
18 | + </li> | ||
19 | +</ul> | ||
20 | + | ||
21 | +**Returns:** | ||
22 | + | ||
23 | +`true` if cell action should be displayed, `false` otherwise. | ||
24 | + | ||
25 | +<div class="divider"></div> | ||
26 | + | ||
27 | +##### Examples | ||
28 | + | ||
29 | +* Display action only for customer users: | ||
30 | + | ||
31 | +```javascript | ||
32 | +return widgetContext.currentUser.authority === 'CUSTOMER_USER'; | ||
33 | +{:copy-code} | ||
34 | +``` | ||
35 | + | ||
36 | +* Display action only if the entity in the row is device and has type `thermostat`: | ||
37 | + | ||
38 | +```javascript | ||
39 | +return data && data.entityType === 'DEVICE' && data.Type === 'thermostat'; | ||
40 | +{:copy-code} | ||
41 | +``` | ||
42 | + | ||
43 | +* Display action only if the entity in the row has `temperature` latest timeseries or attribute value greater than 25: | ||
2 | 44 | ||
3 | ```javascript | 45 | ```javascript |
4 | -return data.entityName === 'Test device'; {:copy-code} | 46 | +return data && data.temperature > 25; |
47 | +{:copy-code} | ||
5 | ``` | 48 | ``` |
@@ -5,15 +5,15 @@ | @@ -5,15 +5,15 @@ | ||
5 | 5 | ||
6 | *function (widgetContext, data): boolean* | 6 | *function (widgetContext, data): boolean* |
7 | 7 | ||
8 | -JavaScript function evaluating whether to display particular widget header action. | 8 | +A JavaScript function evaluating whether to display particular widget header action. |
9 | 9 | ||
10 | **Parameters:** | 10 | **Parameters:** |
11 | 11 | ||
12 | <ul> | 12 | <ul> |
13 | - <li><b>widgetContext</b> - A reference to <a href="https://github.com/thingsboard/thingsboard/blob/5bb6403407aa4898084832d6698aa9ea6d484889/ui-ngx/src/app/modules/home/models/widget-component.models.ts#L107" target="_blank">WidgetContext</a> that has all necessary API<br/> | 13 | + <li><b>widgetContext:</b> <code><a href="https://github.com/thingsboard/thingsboard/blob/5bb6403407aa4898084832d6698aa9ea6d484889/ui-ngx/src/app/modules/home/models/widget-component.models.ts#L107" target="_blank">WidgetContext</a></code> - A reference to <a href="https://github.com/thingsboard/thingsboard/blob/5bb6403407aa4898084832d6698aa9ea6d484889/ui-ngx/src/app/modules/home/models/widget-component.models.ts#L107" target="_blank">WidgetContext</a> that has all necessary API |
14 | and data used by widget instance. | 14 | and data used by widget instance. |
15 | </li> | 15 | </li> |
16 | - <li><b>data</b> - An array of <a href="https://github.com/thingsboard/thingsboard/blob/5bb6403407aa4898084832d6698aa9ea6d484889/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts#L108" target="_blank">FormattedData</a> objects.<br/> | 16 | + <li><b>data:</b> <code><a href="https://github.com/thingsboard/thingsboard/blob/5bb6403407aa4898084832d6698aa9ea6d484889/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts#L108" target="_blank">FormattedData[]</a></code> - An array of <a href="https://github.com/thingsboard/thingsboard/blob/5bb6403407aa4898084832d6698aa9ea6d484889/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts#L108" target="_blank">FormattedData</a> objects.<br/> |
17 | Each object represents basic entity properties (ex. <code>entityId</code>, <code>entityName</code>)<br/>and provides access to other entity attributes/timeseries declared in widget datasource configuration. | 17 | Each object represents basic entity properties (ex. <code>entityId</code>, <code>entityName</code>)<br/>and provides access to other entity attributes/timeseries declared in widget datasource configuration. |
18 | </li> | 18 | </li> |
19 | </ul> | 19 | </ul> |
1 | +#### Data generation function | ||
2 | + | ||
3 | +<div class="divider"></div> | ||
4 | +<br/> | ||
5 | + | ||
6 | +*function (time, prevValue): any* | ||
7 | + | ||
8 | +A JavaScript function generating datapoint values. | ||
9 | + | ||
10 | +**Parameters:** | ||
11 | + | ||
12 | +<ul> | ||
13 | + <li><b>time:</b> <code>number</code> - timestamp in milliseconds of the current datapoint. | ||
14 | + </li> | ||
15 | + <li><b>prevValue:</b> <code>primitive (number/string/boolean)</code> - A previous datapoint value. | ||
16 | + </li> | ||
17 | +</ul> | ||
18 | + | ||
19 | +**Returns:** | ||
20 | + | ||
21 | +A primitive type (number, string or boolean) presenting newly generated datapoint value. | ||
22 | + | ||
23 | +<div class="divider"></div> | ||
24 | + | ||
25 | +##### Examples | ||
26 | + | ||
27 | +* Generate data with sine function: | ||
28 | + | ||
29 | +```javascript | ||
30 | +return Math.sin(time/5000); | ||
31 | +{:copy-code} | ||
32 | +``` | ||
33 | + | ||
34 | +* Generate true/false sequence: | ||
35 | + | ||
36 | +```javascript | ||
37 | +if (!prevValue) { | ||
38 | + return true; | ||
39 | +} else { | ||
40 | + return false; | ||
41 | +} | ||
42 | +{:copy-code} | ||
43 | +``` | ||
44 | + | ||
45 | +* Generate repeating sequence of predefined values (for ex. latitude): | ||
46 | + | ||
47 | +```javascript | ||
48 | +var lats = [37.7696499, | ||
49 | + 37.7699074, | ||
50 | + 37.7699536, | ||
51 | + 37.7697242, | ||
52 | + 37.7695189, | ||
53 | + 37.7696889]; | ||
54 | + | ||
55 | +var index = Math.floor((time/3 % 14000) / 1000); | ||
56 | + | ||
57 | +return lats[index]; | ||
58 | +{:copy-code} | ||
59 | +``` |
1 | +#### Data post-processing function | ||
2 | + | ||
3 | +<div class="divider"></div> | ||
4 | +<br/> | ||
5 | + | ||
6 | +*function (time, value, prevValue, timePrev, prevOrigValue): any* | ||
7 | + | ||
8 | +A JavaScript function doing post-processing on telemetry data. | ||
9 | + | ||
10 | +**Parameters:** | ||
11 | + | ||
12 | +<ul> | ||
13 | + <li><b>time:</b> <code>number</code> - timestamp in milliseconds of the current datapoint. | ||
14 | + </li> | ||
15 | + <li><b>value:</b> <code>primitive (number/string/boolean)</code> - A value of the current datapoint. | ||
16 | + </li> | ||
17 | + <li><b>prevValue:</b> <code>primitive (number/string/boolean)</code> - A value of the previous datapoint after applied post-processing. | ||
18 | + </li> | ||
19 | + <li><b>timePrev:</b> <code>number</code> - timestamp in milliseconds of the previous datapoint value. | ||
20 | + </li> | ||
21 | + <li><b>prevOrigValue:</b> <code>primitive (number/string/boolean)</code> - An original value of the previous datapoint. | ||
22 | + </li> | ||
23 | +</ul> | ||
24 | + | ||
25 | +**Returns:** | ||
26 | + | ||
27 | +A primitive type (number, string or boolean) presenting the new datapoint value. | ||
28 | + | ||
29 | +<div class="divider"></div> | ||
30 | + | ||
31 | +##### Examples | ||
32 | + | ||
33 | +* Multiply all datapoint values by 10: | ||
34 | + | ||
35 | +```javascript | ||
36 | +return value * 10; | ||
37 | +{:copy-code} | ||
38 | +``` | ||
39 | + | ||
40 | +* Round all datapoint values to whole numbers: | ||
41 | + | ||
42 | +```javascript | ||
43 | +return Math.round(value); | ||
44 | +{:copy-code} | ||
45 | +``` | ||
46 | + | ||
47 | +* Get relative difference between data points: | ||
48 | + | ||
49 | +```javascript | ||
50 | +if (prevOrigValue) { | ||
51 | + return (value - prevOrigValue) / prevOrigValue; | ||
52 | +} else { | ||
53 | + return 0; | ||
54 | +} | ||
55 | +{:copy-code} | ||
56 | +``` |
1 | -#### Tooltip value format JavaScript Function | 1 | +#### Tooltip value format function |
2 | 2 | ||
3 | -##### Input arguments | 3 | +<div class="divider"></div> |
4 | +<br/> | ||
4 | 5 | ||
5 | -- value - value to format | 6 | +*function (value): string* |
7 | + | ||
8 | +A JavaScript function used to format datapoint value to be shown on the chart tooltip. | ||
9 | + | ||
10 | +**Parameters:** | ||
11 | + | ||
12 | +<ul> | ||
13 | + <li><b>value:</b> <code>primitive (number/string/boolean)</code> - A value of the datapoint that should be formatted. | ||
14 | + </li> | ||
15 | +</ul> | ||
16 | + | ||
17 | +**Returns:** | ||
18 | + | ||
19 | +A string representing the formatted value. | ||
20 | + | ||
21 | +<div class="divider"></div> | ||
22 | + | ||
23 | +##### Examples | ||
24 | + | ||
25 | +* Present the datapoint value in tooltip in Celsius (°C) units: | ||
6 | 26 | ||
7 | ```javascript | 27 | ```javascript |
8 | -return value; | 28 | +return value + ' °C'; |
9 | {:copy-code} | 29 | {:copy-code} |
10 | ``` | 30 | ``` |
11 | 31 | ||
12 | -##### Examples | 32 | +* Present the datapoint value in tooltip in Amperage (A) units and two decimal places: |
33 | + | ||
34 | +```javascript | ||
35 | +return value.toFixed(2) + ' A'; | ||
36 | +{:copy-code} | ||
37 | +``` |
@@ -516,12 +516,15 @@ mat-label { | @@ -516,12 +516,15 @@ mat-label { | ||
516 | } | 516 | } |
517 | 517 | ||
518 | p, div { | 518 | p, div { |
519 | - padding-left: 32px; | ||
520 | padding-right: 32px; | 519 | padding-right: 32px; |
521 | color: rgba(15, 22, 29, 0.8); | 520 | color: rgba(15, 22, 29, 0.8); |
522 | line-height: 1.5em; | 521 | line-height: 1.5em; |
523 | } | 522 | } |
524 | 523 | ||
524 | + p, div { | ||
525 | + padding-left: 32px; | ||
526 | + } | ||
527 | + | ||
525 | ul { | 528 | ul { |
526 | padding-left: 62px; | 529 | padding-left: 62px; |
527 | padding-right: 32px; | 530 | padding-right: 32px; |
@@ -541,6 +544,12 @@ mat-label { | @@ -541,6 +544,12 @@ mat-label { | ||
541 | li { | 544 | li { |
542 | padding-bottom: .75em; | 545 | padding-bottom: .75em; |
543 | line-height: 1.5em; | 546 | line-height: 1.5em; |
547 | + ul { | ||
548 | + margin-bottom: 0; | ||
549 | + } | ||
550 | + p { | ||
551 | + padding-left: 0; | ||
552 | + } | ||
544 | } | 553 | } |
545 | 554 | ||
546 | a { | 555 | a { |
@@ -651,7 +660,7 @@ mat-label { | @@ -651,7 +660,7 @@ mat-label { | ||
651 | line-height: 24px; | 660 | line-height: 24px; |
652 | color: #fff; | 661 | color: #fff; |
653 | background-color: #305680; | 662 | background-color: #305680; |
654 | - box-shadow: 0px 1px 5px rgba(0, 0, 0, 0.12), 0px 2px 2px rgba(0, 0, 0, 0.14), 0px 3px 1px -2px rgba(0, 0, 0, 0.2); | 663 | + box-shadow: 0 1px 5px rgba(0, 0, 0, 0.12), 0 2px 2px rgba(0, 0, 0, 0.14), 0 3px 1px -2px rgba(0, 0, 0, 0.2); |
655 | text-decoration: none; | 664 | text-decoration: none; |
656 | font-size: 16px; | 665 | font-size: 16px; |
657 | font-weight: 500; | 666 | font-weight: 500; |
@@ -777,7 +786,7 @@ mat-label { | @@ -777,7 +786,7 @@ mat-label { | ||
777 | 786 | ||
778 | button.clipboard-btn { | 787 | button.clipboard-btn { |
779 | top: -10px; | 788 | top: -10px; |
780 | - right: 0px; | 789 | + right: 0; |
781 | padding: 0 3px; | 790 | padding: 0 3px; |
782 | } | 791 | } |
783 | } | 792 | } |
@@ -1824,10 +1824,10 @@ | @@ -1824,10 +1824,10 @@ | ||
1824 | resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.170.tgz#0d67711d4bf7f4ca5147e9091b847479b87925d6" | 1824 | resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.170.tgz#0d67711d4bf7f4ca5147e9091b847479b87925d6" |
1825 | integrity sha512-bpcvu/MKHHeYX+qeEN8GE7DIravODWdACVA1ctevD8CN24RhPZIKMn9ntfAsrvLfSX3cR5RrBKAbYm9bGs0A+Q== | 1825 | integrity sha512-bpcvu/MKHHeYX+qeEN8GE7DIravODWdACVA1ctevD8CN24RhPZIKMn9ntfAsrvLfSX3cR5RrBKAbYm9bGs0A+Q== |
1826 | 1826 | ||
1827 | -"@types/marked@^1.1.0": | ||
1828 | - version "1.2.2" | ||
1829 | - resolved "https://registry.yarnpkg.com/@types/marked/-/marked-1.2.2.tgz#1f858a0e690247ecf3b2eef576f98f86e8d960d4" | ||
1830 | - integrity sha512-wLfw1hnuuDYrFz97IzJja0pdVsC0oedtS4QsKH1/inyW9qkLQbXgMUqEQT0MVtUBx3twjWeInUfjQbhBVLECXw== | 1827 | +"@types/marked@^2.0.0": |
1828 | + version "2.0.5" | ||
1829 | + resolved "https://registry.yarnpkg.com/@types/marked/-/marked-2.0.5.tgz#453e27f1e97199d45bb25297b0dd2b9bbc1e05ea" | ||
1830 | + integrity sha512-shRZ7XnYFD/8n8zSjKvFdto1QNSf4tONZIlNEZGrJe8GsOE8DL/hG1Hbl8gZlfLnjS7+f5tZGIaTgfpyW38h4w== | ||
1831 | 1831 | ||
1832 | "@types/minimatch@*": | 1832 | "@types/minimatch@*": |
1833 | version "3.0.3" | 1833 | version "3.0.3" |
@@ -3268,6 +3268,11 @@ commander@^2.11.0, commander@^2.12.1, commander@^2.19.0, commander@^2.20.0: | @@ -3268,6 +3268,11 @@ commander@^2.11.0, commander@^2.12.1, commander@^2.19.0, commander@^2.20.0: | ||
3268 | resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" | 3268 | resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" |
3269 | integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== | 3269 | integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== |
3270 | 3270 | ||
3271 | +commander@^6.0.0: | ||
3272 | + version "6.2.1" | ||
3273 | + resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" | ||
3274 | + integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== | ||
3275 | + | ||
3271 | commander@^7.1.0: | 3276 | commander@^7.1.0: |
3272 | version "7.2.0" | 3277 | version "7.2.0" |
3273 | resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" | 3278 | resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" |
@@ -6171,12 +6176,12 @@ karma@~6.3.2: | @@ -6171,12 +6176,12 @@ karma@~6.3.2: | ||
6171 | ua-parser-js "^0.7.23" | 6176 | ua-parser-js "^0.7.23" |
6172 | yargs "^16.1.1" | 6177 | yargs "^16.1.1" |
6173 | 6178 | ||
6174 | -katex@^0.12.0: | ||
6175 | - version "0.12.0" | ||
6176 | - resolved "https://registry.yarnpkg.com/katex/-/katex-0.12.0.tgz#2fb1c665dbd2b043edcf8a1f5c555f46beaa0cb9" | ||
6177 | - integrity sha512-y+8btoc/CK70XqcHqjxiGWBOeIL8upbS0peTPXTvgrh21n1RiWWcIpSWM+4uXq+IAgNh9YYQWdc7LVDPDAEEAg== | 6179 | +katex@^0.13.0: |
6180 | + version "0.13.18" | ||
6181 | + resolved "https://registry.yarnpkg.com/katex/-/katex-0.13.18.tgz#ba89e8e4b70cc2325e25e019a62b9fe71e5c2931" | ||
6182 | + integrity sha512-a3dC4NSVSDU3O1WZbTnOiA8rVNJ2lSiomOl0kmckCIGObccIHXof7gAseIY0o1gjEspe+34ZeSEX2D1ChFKIvA== | ||
6178 | dependencies: | 6183 | dependencies: |
6179 | - commander "^2.19.0" | 6184 | + commander "^6.0.0" |
6180 | 6185 | ||
6181 | killable@^1.0.1: | 6186 | killable@^1.0.1: |
6182 | version "1.0.1" | 6187 | version "1.0.1" |
@@ -6484,10 +6489,10 @@ map-visit@^1.0.0: | @@ -6484,10 +6489,10 @@ map-visit@^1.0.0: | ||
6484 | dependencies: | 6489 | dependencies: |
6485 | object-visit "^1.0.0" | 6490 | object-visit "^1.0.0" |
6486 | 6491 | ||
6487 | -marked@^1.1.0: | ||
6488 | - version "1.2.9" | ||
6489 | - resolved "https://registry.yarnpkg.com/marked/-/marked-1.2.9.tgz#53786f8b05d4c01a2a5a76b7d1ec9943d29d72dc" | ||
6490 | - integrity sha512-H8lIX2SvyitGX+TRdtS06m1jHMijKN/XjfH6Ooii9fvxMlh8QdqBfBDkGUpMWH2kQNrtixjzYUa3SH8ROTgRRw== | 6492 | +marked@^2.0.0: |
6493 | + version "2.1.3" | ||
6494 | + resolved "https://registry.yarnpkg.com/marked/-/marked-2.1.3.tgz#bd017cef6431724fd4b27e0657f5ceb14bff3753" | ||
6495 | + integrity sha512-/Q+7MGzaETqifOMWYEA7HVMaZb4XbcRfaOzcSsHZEith83KGlvaSG33u0SKu89Mj5h+T8V2hM+8O45Qc5XTgwA== | ||
6491 | 6496 | ||
6492 | material-design-icons@^3.0.1: | 6497 | material-design-icons@^3.0.1: |
6493 | version "3.0.1" | 6498 | version "3.0.1" |
@@ -6941,16 +6946,16 @@ ngx-hm-carousel@^2.0.0-rc.1: | @@ -6941,16 +6946,16 @@ ngx-hm-carousel@^2.0.0-rc.1: | ||
6941 | hammerjs "^2.0.8" | 6946 | hammerjs "^2.0.8" |
6942 | resize-observer-polyfill "^1.5.1" | 6947 | resize-observer-polyfill "^1.5.1" |
6943 | 6948 | ||
6944 | -ngx-markdown@^10.1.1: | ||
6945 | - version "10.1.1" | ||
6946 | - resolved "https://registry.yarnpkg.com/ngx-markdown/-/ngx-markdown-10.1.1.tgz#17840c773db7ced4b18ccbf2e8cb06182e422de3" | ||
6947 | - integrity sha512-bUVgN6asb35d5U4xM5CNfo7pSpuwqJSdTgK0PhNZzLiaiyPIK2owtLF6sWGhxTThJu+LngJPjj4MQ+AFe/s8XQ== | 6949 | +ngx-markdown@^11.1.3: |
6950 | + version "11.1.3" | ||
6951 | + resolved "https://registry.yarnpkg.com/ngx-markdown/-/ngx-markdown-11.1.3.tgz#4d01c2dc425e5a46d1b6aa8517d9a1c1feaa1efd" | ||
6952 | + integrity sha512-z32q8l76ubrcP62L03mdvrizwueLBHV10LkT8MEDnFcjmY+8J1PytxFJ9EBTJpvc+CaPolgAoi7felN2XJZTSg== | ||
6948 | dependencies: | 6953 | dependencies: |
6949 | - "@types/marked" "^1.1.0" | 6954 | + "@types/marked" "^2.0.0" |
6950 | emoji-toolkit "^6.0.1" | 6955 | emoji-toolkit "^6.0.1" |
6951 | - katex "^0.12.0" | ||
6952 | - marked "^1.1.0" | ||
6953 | - prismjs "^1.20.0" | 6956 | + katex "^0.13.0" |
6957 | + marked "^2.0.0" | ||
6958 | + prismjs "^1.23.0" | ||
6954 | tslib "^2.0.0" | 6959 | tslib "^2.0.0" |
6955 | 6960 | ||
6956 | ngx-sharebuttons@^8.0.5: | 6961 | ngx-sharebuttons@^8.0.5: |
@@ -7985,11 +7990,6 @@ pretty-bytes@^5.3.0: | @@ -7985,11 +7990,6 @@ pretty-bytes@^5.3.0: | ||
7985 | resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" | 7990 | resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" |
7986 | integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg== | 7991 | integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg== |
7987 | 7992 | ||
7988 | -prismjs@^1.20.0: | ||
7989 | - version "1.24.1" | ||
7990 | - resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.24.1.tgz#c4d7895c4d6500289482fa8936d9cdd192684036" | ||
7991 | - integrity sha512-mNPsedLuk90RVJioIky8ANZEwYm5w9LcvCXrxHlwf4fNVSn8jEipMybMkWUyyF0JhnC+C4VcOVSBuHRKs1L5Ow== | ||
7992 | - | ||
7993 | prismjs@^1.23.0: | 7993 | prismjs@^1.23.0: |
7994 | version "1.23.0" | 7994 | version "1.23.0" |
7995 | resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.23.0.tgz#d3b3967f7d72440690497652a9d40ff046067f33" | 7995 | resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.23.0.tgz#d3b3967f7d72440690497652a9d40ff046067f33" |