Commit b4f230dbc8dca59d43a831ab22d20d5a562475aa

Authored by Igor Kulikov
1 parent e264f7b8

Improve markdown component. Add new helps.

Showing 39 changed files with 1724 additions and 247 deletions
... ... @@ -73,7 +73,7 @@
73 73 "ngx-drag-drop": "^2.0.0",
74 74 "ngx-flowchart": "git://github.com/thingsboard/ngx-flowchart.git#master",
75 75 "ngx-hm-carousel": "^2.0.0-rc.1",
76   - "ngx-markdown": "^10.1.1",
  76 + "ngx-markdown": "^11.1.3",
77 77 "ngx-sharebuttons": "^8.0.5",
78 78 "ngx-translate-messageformat-compiler": "^4.9.0",
79 79 "objectpath": "^2.0.0",
... ...
... ... @@ -55,11 +55,12 @@ export class DynamicComponentFactoryService {
55 55 public createDynamicComponentFactory<T>(
56 56 componentType: Type<T>,
57 57 template: string,
58   - modules?: Type<any>[]): Observable<ComponentFactory<T>> {
  58 + modules?: Type<any>[],
  59 + preserveWhitespaces?: boolean): Observable<ComponentFactory<T>> {
59 60 const dymamicComponentFactorySubject = new ReplaySubject<ComponentFactory<T>>();
60 61 import('@angular/compiler').then(
61 62 () => {
62   - const comp = this.createDynamicComponent(componentType, template);
  63 + const comp = this.createDynamicComponent(componentType, template, preserveWhitespaces);
63 64 let moduleImports: Type<any>[] = [CommonModule];
64 65 if (modules) {
65 66 moduleImports = [...moduleImports, ...modules];
... ... @@ -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 108 // noinspection AngularMissingOrInvalidDeclarationInModule
108 109 return Component({
109   - template
  110 + template,
  111 + preserveWhitespaces
110 112 })(componentType);
111 113 }
112 114
... ...
... ... @@ -18,7 +18,8 @@ import { Injectable } from '@angular/core';
18 18 import { HttpClient } from '@angular/common/http';
19 19 import { TranslateService } from '@ngx-translate/core';
20 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 24 const NOT_FOUND_CONTENT = '## Not found';
24 25
... ... @@ -27,6 +28,8 @@ const NOT_FOUND_CONTENT = '## Not found';
27 28 })
28 29 export class HelpService {
29 30
  31 + private helpBaseUrl = helpBaseUrl;
  32 +
30 33 private helpCache: {[lang: string]: {[key: string]: string}} = {};
31 34
32 35 constructor(
... ... @@ -52,6 +55,9 @@ export class HelpService {
52 55 return of(NOT_FOUND_CONTENT);
53 56 }
54 57 }),
  58 + mergeMap((content) => {
  59 + return this.processIncludes(this.processVariables(content));
  60 + }),
55 61 tap((content) => {
56 62 let langContent = this.helpCache[lang];
57 63 if (!langContent) {
... ... @@ -68,4 +74,25 @@ export class HelpService {
68 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 46 [functionArgs]="['$event', 'widgetContext', 'entityId', 'entityName', 'htmlTemplate', 'additionalParams', 'entityLabel']"
47 47 [disableUndefinedCheck]="true"
48 48 [validationArgs]="[]"
49   - [editorCompleter]="customPrettyActionEditorCompleter">
  49 + [editorCompleter]="customPrettyActionEditorCompleter"
  50 + helpId="widget/action/custom_pretty_action_fn">
50 51 </tb-js-func>
51 52 </div>
52 53 </div>
... ...
... ... @@ -96,7 +96,8 @@
96 96 [functionArgs]="['$event', 'widgetContext', 'entityId', 'entityName', 'htmlTemplate', 'additionalParams', 'entityLabel']"
97 97 [disableUndefinedCheck]="true"
98 98 [validationArgs]="[]"
99   - [editorCompleter]="customPrettyActionEditorCompleter">
  99 + [editorCompleter]="customPrettyActionEditorCompleter"
  100 + helpId="widget/action/custom_pretty_action_fn">
100 101 </tb-js-func>
101 102 </mat-tab>
102 103 </mat-tab-group>
... ...
... ... @@ -224,6 +224,7 @@
224 224 [globalVariables]="functionScopeVariables"
225 225 [validationArgs]="[]"
226 226 [editorCompleter]="customActionEditorCompleter"
  227 + helpId="widget/action/custom_action_fn"
227 228 ></tb-js-func>
228 229 </ng-template>
229 230 <ng-template [ngSwitchCase]="widgetActionType.customPretty">
... ...
... ... @@ -70,6 +70,7 @@
70 70 [globalVariables]="functionScopeVariables"
71 71 [validationArgs]="[[1, 1],[1, '1']]"
72 72 resultType="any"
  73 + helpId="widget/config/datakey_generation_fn"
73 74 formControlName="funcBody">
74 75 </tb-js-func>
75 76 </section>
... ... @@ -82,6 +83,7 @@
82 83 [globalVariables]="functionScopeVariables"
83 84 [validationArgs]="[[1, 1, 1, 1, 1],[1, '1', '1', 1, '1']]"
84 85 resultType="any"
  86 + helpId="widget/config/datakey_postprocess_fn"
85 87 formControlName="postFuncBody">
86 88 </tb-js-func>
87 89 <label *ngIf="dataKeyFormGroup.get('usePostProcessing').value" class="tb-title" style="margin-left: 15px;">
... ...
... ... @@ -15,4 +15,4 @@
15 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 107 import { EMBED_DASHBOARD_DIALOG_TOKEN } from '@home/components/widget/dialog/embed-dashboard-dialog-token';
108 108 import { MobileService } from '@core/services/mobile.service';
109 109 import { DialogService } from '@core/services/dialog.service';
110   -import { TbPopoverService } from '@shared/components/popover.component';
111 110 import { DashboardPageComponent } from '@home/components/dashboard-page/dashboard-page.component';
112 111 import { PopoverPlacement } from '@shared/components/popover.models';
  112 +import { TbPopoverService } from '@shared/components/popover.service';
113 113
114 114 @Component({
115 115 selector: 'tb-widget',
... ...
... ... @@ -16,5 +16,5 @@
16 16
17 17 -->
18 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 20 </ng-container>
... ...
... ... @@ -13,16 +13,14 @@
13 13 * See the License for the specific language governing permissions and
14 14 * limitations under the License.
15 15 */
16   -:host {
  16 +
  17 +:host ::ng-deep {
17 18 .tb-help-markdown {
18 19 overflow: auto;
19 20 max-width: 80vw;
20 21 max-height: 80vh;
21 22 margin-top: 30px;
22 23 }
23   -}
24   -
25   -:host ::ng-deep {
26 24 .tb-help-markdown.tb-markdown-view {
27 25 h1, h2, h3, h4, h5, h6 {
28 26 &:first-child {
... ...
... ... @@ -22,7 +22,7 @@ import {
22 22 Output, SimpleChanges
23 23 } from '@angular/core';
24 24 import { BehaviorSubject } from 'rxjs';
25   -import { delay, share } from 'rxjs/operators';
  25 +import { share } from 'rxjs/operators';
26 26 import { HelpService } from '@core/services/help.service';
27 27
28 28 @Component({
... ... @@ -34,8 +34,12 @@ export class HelpMarkdownComponent implements OnDestroy, OnInit, OnChanges {
34 34
35 35 @Input() helpId: string;
36 36
  37 + @Input() helpContent: string;
  38 +
37 39 @Input() visible: boolean;
38 40
  41 + @Input() style: { [klass: string]: any } = {};
  42 +
39 43 @Output() markdownReady = new EventEmitter<void>();
40 44
41 45 markdownText = new BehaviorSubject<string>(null);
... ... @@ -44,8 +48,6 @@ export class HelpMarkdownComponent implements OnDestroy, OnInit, OnChanges {
44 48 share()
45 49 );
46 50
47   - isMarkdownReady = false;
48   -
49 51 private loadHelpPending = false;
50 52
51 53 constructor(private help: HelpService) {}
... ... @@ -68,7 +70,7 @@ export class HelpMarkdownComponent implements OnDestroy, OnInit, OnChanges {
68 70 this.loadHelp();
69 71 }
70 72 }
71   - if (propName === 'helpId') {
  73 + if (propName === 'helpId' || propName === 'helpContent') {
72 74 this.markdownText.next(null);
73 75 this.loadHelpWhenVisible();
74 76 }
... ... @@ -89,16 +91,16 @@ export class HelpMarkdownComponent implements OnDestroy, OnInit, OnChanges {
89 91 this.help.getHelpContent(this.helpId).subscribe((content) => {
90 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 99 onMarkdownReady() {
96   - this.isMarkdownReady = true;
97 100 this.markdownReady.next();
98 101 }
99 102
100 103 markdownClick($event: MouseEvent) {
101   -
102 104 }
103 105
104 106 }
... ...
... ... @@ -15,16 +15,22 @@
15 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 14 * limitations under the License.
15 15 */
16 16 .tb-help-popup-button {
  17 + position: relative;
17 18 }
18 19 .tb-help-popup-button-loading {
19 20 background: #fff;
20 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 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 30 @Component({
21 31 // tslint:disable-next-line:component-selector
... ... @@ -26,25 +36,41 @@ import { TbPopoverService } from '@shared/components/popover.component';
26 36 })
27 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 42 // tslint:disable-next-line:no-input-rename
30 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 54 popoverVisible = false;
33 55 popoverReady = true;
34 56
35   - constructor(private elementRef: ElementRef,
36   - private viewContainerRef: ViewContainerRef,
  57 + constructor(private viewContainerRef: ViewContainerRef,
37 58 private renderer: Renderer2,
38 59 private popoverService: TbPopoverService) {}
39 60
40 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 64 this.helpId,
  65 + '',
43 66 (visible) => {
44 67 this.popoverVisible = visible;
45 68 }, (ready => {
46 69 this.popoverReady = ready;
47   - }));
  70 + }),
  71 + this.helpPopupPlacement,
  72 + {},
  73 + this.helpPopupStyle);
48 74 }
49 75
50 76 ngOnDestroy(): void {
... ...
... ... @@ -46,8 +46,7 @@ import { GroupInfo } from '@shared/models/widget.models';
46 46 import { Observable } from 'rxjs/internal/Observable';
47 47 import { forkJoin, from } from 'rxjs';
48 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 51 const tinycolor = tinycolor_;
53 52
... ... @@ -252,7 +251,7 @@ export class JsonFormComponent implements OnInit, ControlValueAccessor, Validato
252 251
253 252 private onHelpClick(event: MouseEvent, helpId: string, helpVisibleFn: (visible: boolean) => void, helpReadyFn: (ready: boolean) => void) {
254 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 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, '&#123;').replace(/}/g, '&#125;');
  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 85
86 86 private wrapCopyCode(id: number, content: string, code: string): string {
87 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 89 `<button class="clipboard-btn">\n` +
90 90 ` <p>${this.translate.instant('markdown.copy-code')}</p>\n` +
91 91 ` <div>\n` +
... ... @@ -119,7 +119,7 @@ export class MarkedOptionsService extends MarkedOptions {
119 119 private markdownCopyCode(id: number) {
120 120 const copyWrapper = $('#codeWrapper' + id);
121 121 if (copyWrapper.hasClass('noChars')) {
122   - const text = $('#copyCodeId' + id).text();
  122 + const text = decodeURIComponent($('#copyCodeId' + id).text());
123 123 this.window.navigator.clipboard.writeText(text).then(() => {
124 124 import('tooltipster').then(
125 125 () => {
... ...
... ... @@ -25,7 +25,6 @@ import {
25 25 Directive,
26 26 ElementRef,
27 27 EventEmitter,
28   - Injectable,
29 28 Injector,
30 29 Input,
31 30 OnChanges,
... ... @@ -36,7 +35,6 @@ import {
36 35 Renderer2,
37 36 SimpleChanges,
38 37 TemplateRef,
39   - Type,
40 38 ViewChild,
41 39 ViewContainerRef,
42 40 ViewEncapsulation
... ... @@ -54,13 +52,11 @@ import {
54 52 getPlacementName,
55 53 popoverMotion,
56 54 PopoverPlacement,
57   - PopoverWithTrigger,
58 55 POSITION_MAP,
59 56 PropertyMapping
60 57 } from '@shared/components/popover.models';
61 58 import { distinctUntilChanged, takeUntil } from 'rxjs/operators';
62 59 import { isNotEmptyStr, onParentScrollOrWindowResize } from '@core/utils';
63   -import { HelpMarkdownComponent } from '@shared/components/help-markdown.component';
64 60
65 61 export type TbPopoverTrigger = 'click' | 'focus' | 'hover' | null;
66 62
... ... @@ -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 284 @Component({
445 285 selector: 'tb-popover',
446 286 exportAs: 'tbPopoverComponent',
... ... @@ -703,10 +543,12 @@ export class TbPopoverComponent implements OnDestroy, OnInit {
703 543
704 544 updateStyles(): void {
705 545 this.classMap = {
706   - [this.tbOverlayClassName]: true,
707 546 [`tb-popover-placement-${this.preferredPlacement}`]: true,
708 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 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 +}
... ...
  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 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 59 export const HelpLinks = {
60 60 linksMap: {
... ...
... ... @@ -147,11 +147,14 @@ import { MAT_DATE_LOCALE } from '@angular/material/core';
147 147 import { CopyButtonComponent } from '@shared/components/button/copy-button.component';
148 148 import { TogglePasswordComponent } from '@shared/components/button/toggle-password.component';
149 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 151 import { TbStringTemplateOutletDirective } from '@shared/components/directives/sring-template-outlet.directive';
152 152 import { TbComponentOutletDirective} from '@shared/components/directives/component-outlet.directive';
153 153 import { HelpMarkdownComponent } from '@shared/components/help-markdown.component';
154 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 159 export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) {
157 160 return markedOptionsService;
... ... @@ -174,6 +177,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
174 177 provide: MAT_DATE_LOCALE,
175 178 useValue: 'en-GB'
176 179 },
  180 + { provide: HELP_MARKDOWN_COMPONENT_TOKEN, useValue: HelpMarkdownComponent },
  181 + { provide: SHARED_MODULE_TOKEN, useValue: SharedModule },
177 182 TbPopoverService
178 183 ],
179 184 declarations: [
... ... @@ -190,6 +195,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
190 195 TbStringTemplateOutletDirective,
191 196 TbComponentOutletDirective,
192 197 TbPopoverDirective,
  198 + TbMarkdownComponent,
193 199 HelpComponent,
194 200 HelpMarkdownComponent,
195 201 HelpPopupComponent,
... ... @@ -336,6 +342,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
336 342 TbStringTemplateOutletDirective,
337 343 TbComponentOutletDirective,
338 344 TbPopoverDirective,
  345 + TbMarkdownComponent,
339 346 HelpComponent,
340 347 HelpMarkdownComponent,
341 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 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 5
6 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 10 **Parameters:**
11 11
12 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 14 and data used by widget instance.
15 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 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 18 </li>
19 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 27 ```javascript
8   -return value;
  28 +return value + ' °C';
9 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 516 }
517 517
518 518 p, div {
519   - padding-left: 32px;
520 519 padding-right: 32px;
521 520 color: rgba(15, 22, 29, 0.8);
522 521 line-height: 1.5em;
523 522 }
524 523
  524 + p, div {
  525 + padding-left: 32px;
  526 + }
  527 +
525 528 ul {
526 529 padding-left: 62px;
527 530 padding-right: 32px;
... ... @@ -541,6 +544,12 @@ mat-label {
541 544 li {
542 545 padding-bottom: .75em;
543 546 line-height: 1.5em;
  547 + ul {
  548 + margin-bottom: 0;
  549 + }
  550 + p {
  551 + padding-left: 0;
  552 + }
544 553 }
545 554
546 555 a {
... ... @@ -651,7 +660,7 @@ mat-label {
651 660 line-height: 24px;
652 661 color: #fff;
653 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 664 text-decoration: none;
656 665 font-size: 16px;
657 666 font-weight: 500;
... ... @@ -777,7 +786,7 @@ mat-label {
777 786
778 787 button.clipboard-btn {
779 788 top: -10px;
780   - right: 0px;
  789 + right: 0;
781 790 padding: 0 3px;
782 791 }
783 792 }
... ...
... ... @@ -1824,10 +1824,10 @@
1824 1824 resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.170.tgz#0d67711d4bf7f4ca5147e9091b847479b87925d6"
1825 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 1832 "@types/minimatch@*":
1833 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 3268 resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
3269 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 3276 commander@^7.1.0:
3272 3277 version "7.2.0"
3273 3278 resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"
... ... @@ -6171,12 +6176,12 @@ karma@~6.3.2:
6171 6176 ua-parser-js "^0.7.23"
6172 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 6183 dependencies:
6179   - commander "^2.19.0"
  6184 + commander "^6.0.0"
6180 6185
6181 6186 killable@^1.0.1:
6182 6187 version "1.0.1"
... ... @@ -6484,10 +6489,10 @@ map-visit@^1.0.0:
6484 6489 dependencies:
6485 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 6497 material-design-icons@^3.0.1:
6493 6498 version "3.0.1"
... ... @@ -6941,16 +6946,16 @@ ngx-hm-carousel@^2.0.0-rc.1:
6941 6946 hammerjs "^2.0.8"
6942 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 6953 dependencies:
6949   - "@types/marked" "^1.1.0"
  6954 + "@types/marked" "^2.0.0"
6950 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 6959 tslib "^2.0.0"
6955 6960
6956 6961 ngx-sharebuttons@^8.0.5:
... ... @@ -7985,11 +7990,6 @@ pretty-bytes@^5.3.0:
7985 7990 resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb"
7986 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 7993 prismjs@^1.23.0:
7994 7994 version "1.23.0"
7995 7995 resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.23.0.tgz#d3b3967f7d72440690497652a9d40ff046067f33"
... ...