Browse Source

Improve markdown component. Add more help content.

pull/5552/head
Igor Kulikov 5 years ago
parent
commit
62823f6f73
  1. 1
      ui-ngx/angular.json
  2. 8
      ui-ngx/src/app/modules/home/components/widget/action/mobile-action-editor.component.html
  3. 6
      ui-ngx/src/app/modules/home/components/widget/action/mobile-action-editor.models.ts
  4. 2
      ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.html
  5. 2
      ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.html
  6. 1
      ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html
  7. 18
      ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.scss
  8. 4
      ui-ngx/src/app/shared/components/help-markdown.component.html
  9. 26
      ui-ngx/src/app/shared/components/help-popup.component.html
  10. 56
      ui-ngx/src/app/shared/components/help-popup.component.scss
  11. 32
      ui-ngx/src/app/shared/components/help-popup.component.ts
  12. 26
      ui-ngx/src/app/shared/components/markdown.component.html
  13. 75
      ui-ngx/src/app/shared/components/markdown.component.ts
  14. 26
      ui-ngx/src/app/shared/components/marked-options.service.ts
  15. 19
      ui-ngx/src/assets/help/en_US/widget/action/custom_action_args.md
  16. 13
      ui-ngx/src/assets/help/en_US/widget/action/custom_action_fn.md
  17. 97
      ui-ngx/src/assets/help/en_US/widget/action/custom_additional_params.md
  18. 13
      ui-ngx/src/assets/help/en_US/widget/action/custom_pretty_action_fn.md
  19. 49
      ui-ngx/src/assets/help/en_US/widget/action/mobile_get_location_fn.md
  20. 48
      ui-ngx/src/assets/help/en_US/widget/action/mobile_get_phone_number_fn.md
  21. 31
      ui-ngx/src/assets/help/en_US/widget/action/mobile_handle_empty_result_fn.md
  22. 33
      ui-ngx/src/assets/help/en_US/widget/action/mobile_handle_error_fn.md
  23. 92
      ui-ngx/src/assets/help/en_US/widget/action/mobile_process_image_fn.md
  24. 33
      ui-ngx/src/assets/help/en_US/widget/action/mobile_process_launch_result_fn.md
  25. 59
      ui-ngx/src/assets/help/en_US/widget/action/mobile_process_location_fn.md
  26. 76
      ui-ngx/src/assets/help/en_US/widget/action/mobile_process_qr_code_fn.md
  27. 147
      ui-ngx/src/assets/help/en_US/widget/editor/examples/alarm_widget.md
  28. 67
      ui-ngx/src/assets/help/en_US/widget/editor/examples/ext_latest_values_example.md
  29. 112
      ui-ngx/src/assets/help/en_US/widget/editor/examples/ext_timeseries_example.md
  30. 47
      ui-ngx/src/assets/help/en_US/widget/editor/examples/latest_values_widget.md
  31. 187
      ui-ngx/src/assets/help/en_US/widget/editor/examples/rpc_widget.md
  32. 72
      ui-ngx/src/assets/help/en_US/widget/editor/examples/static_widget.md
  33. 93
      ui-ngx/src/assets/help/en_US/widget/editor/examples/timeseries_widget.md
  34. 17
      ui-ngx/src/assets/help/en_US/widget/editor/widget_js_action_sources_object.md
  35. 62
      ui-ngx/src/assets/help/en_US/widget/editor/widget_js_existing_code.md
  36. 135
      ui-ngx/src/assets/help/en_US/widget/editor/widget_js_fn.md
  37. 113
      ui-ngx/src/assets/help/en_US/widget/editor/widget_js_subscription_object.md
  38. 14
      ui-ngx/src/assets/help/en_US/widget/editor/widget_js_type_parameters_object.md
  39. 15
      ui-ngx/src/styles.scss

1
ui-ngx/angular.json

@ -97,6 +97,7 @@
"node_modules/prismjs/components/prism-bash.min.js",
"node_modules/prismjs/components/prism-json.min.js",
"node_modules/prismjs/components/prism-javascript.min.js",
"node_modules/prismjs/components/prism-typescript.min.js",
"node_modules/prismjs/plugins/line-numbers/prism-line-numbers.js"
],
"customWebpackConfig": {

8
ui-ngx/src/app/modules/home/components/widget/action/mobile-action-editor.component.html

@ -37,6 +37,7 @@
[functionArgs]="['$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']"
[globalVariables]="functionScopeVariables"
[editorCompleter]="customActionEditorCompleter"
helpId="widget/action/mobile_get_location_fn"
></tb-js-func>
</ng-template>
<ng-template [ngSwitchCase]="mobileActionType.makePhoneCall">
@ -46,6 +47,7 @@
[functionArgs]="['$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']"
[globalVariables]="functionScopeVariables"
[editorCompleter]="customActionEditorCompleter"
helpId="widget/action/mobile_get_phone_number_fn"
></tb-js-func>
</ng-template>
<ng-template [ngSwitchCase]="mobileActionFormGroup.get('type').value === mobileActionType.takePhoto ||
@ -58,6 +60,7 @@
[functionArgs]="['imageUrl', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']"
[globalVariables]="functionScopeVariables"
[editorCompleter]="customActionEditorCompleter"
helpId="widget/action/mobile_process_image_fn"
></tb-js-func>
</ng-template>
<ng-template [ngSwitchCase]="mobileActionType.scanQrCode">
@ -67,6 +70,7 @@
[functionArgs]="['code', 'format', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']"
[globalVariables]="functionScopeVariables"
[editorCompleter]="customActionEditorCompleter"
helpId="widget/action/mobile_process_qr_code_fn"
></tb-js-func>
</ng-template>
<ng-template [ngSwitchCase]="mobileActionType.getLocation">
@ -76,6 +80,7 @@
[functionArgs]="['latitude', 'longitude', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']"
[globalVariables]="functionScopeVariables"
[editorCompleter]="customActionEditorCompleter"
helpId="widget/action/mobile_process_location_fn"
></tb-js-func>
</ng-template>
<ng-template [ngSwitchCase]="mobileActionFormGroup.get('type').value === mobileActionType.mapDirection ||
@ -88,6 +93,7 @@
[functionArgs]="['launched', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']"
[globalVariables]="functionScopeVariables"
[editorCompleter]="customActionEditorCompleter"
helpId="widget/action/mobile_process_launch_result_fn"
></tb-js-func>
</ng-template>
</div>
@ -97,6 +103,7 @@
[functionArgs]="['$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']"
[globalVariables]="functionScopeVariables"
[editorCompleter]="customActionEditorCompleter"
helpId="widget/action/mobile_handle_empty_result_fn"
></tb-js-func>
<tb-js-func *ngIf="mobileActionFormGroup.get('type').value"
formControlName="handleErrorFunction"
@ -104,5 +111,6 @@
[functionArgs]="['error', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']"
[globalVariables]="functionScopeVariables"
[editorCompleter]="customActionEditorCompleter"
helpId="widget/action/mobile_handle_error_fn"
></tb-js-func>
</div>

6
ui-ngx/src/app/modules/home/components/widget/action/mobile-action-editor.models.ts

@ -111,12 +111,6 @@ const processLocationFunction =
'showLocationDialog(\'Location\', latitude, longitude);\n' +
'// saveEntityLocationAttributes(\'latitude\', \'longitude\', latitude, longitude);\n' +
'\n' +
'function showImageDialog(title, imageUrl) {\n' +
' setTimeout(function() {\n' +
' widgetContext.customDialog.customDialog(imageDialogTemplate, ImageDialogController, {imageUrl: imageUrl, title: title}).subscribe();\n' +
' }, 100);\n' +
'}\n' +
'\n' +
'function saveEntityLocationAttributes(latitudeAttributeName, longitudeAttributeName, latitude, longitude) {\n' +
' if (entityId) {\n' +
' let attributes = [\n' +

2
ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.html

@ -15,7 +15,7 @@
limitations under the License.
-->
<form [formGroup]="dataKeyFormGroup" (ngSubmit)="save()" style="min-width: 480px;">
<form [formGroup]="dataKeyFormGroup" (ngSubmit)="save()" style="width: 700px;">
<mat-toolbar color="primary">
<h2>{{ 'datakey.configuration' | translate }}</h2>
<span fxFlex></span>

2
ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.html

@ -15,4 +15,4 @@
limitations under the License.
-->
<tb-markdown [data]="markdownText" lineNumbers (click)="markdownClick($event)"></tb-markdown>
<tb-markdown [data]="markdownText" lineNumbers fallbackToPlainMarkdown (click)="markdownClick($event)"></tb-markdown>

1
ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html

@ -251,6 +251,7 @@
<button mat-button (click)="beautifyJs()">
{{ 'widget.tidy' | translate }}
</button>
<span tb-help-popup="widget/editor/widget_js_fn" tb-help-popup-placement="top" [tb-help-popup-style]="{maxWidth: '1200px'}"></span>
<button mat-icon-button class="tb-mat-32"
(click)="javascriptFullscreen = !javascriptFullscreen"
matTooltip="{{(javascriptFullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}"

18
ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.scss

@ -117,14 +117,17 @@ div.tb-editor-area-title-panel {
font-size: .8rem;
font-weight: 500;
& > * {
&:not(:last-child) {
margin-right: 4px;
}
}
label {
padding: 4px;
color: #00acc1;
background: rgba(220, 220, 220, .35);
border-radius: 5px;
&:not(:last-child) {
margin-right: 4px;
}
}
button.mat-button, button.mat-icon-button, button.mat-icon-button.tb-mat-32 {
@ -133,15 +136,16 @@ div.tb-editor-area-title-panel {
min-width: 32px;
min-height: 15px;
padding: 4px;
margin: 0;
font-size: .8rem;
line-height: 15px;
color: #7b7b7b;
background: rgba(220, 220, 220, .35);
&:not(:last-child) {
margin-right: 4px;
&:not(.tb-help-popup-button) {
color: #7b7b7b;
}
}
.tb-help-popup-button-loading {
background: #f3f3f3;
}
}
.tb-resize-container {

4
ui-ngx/src/app/shared/components/help-markdown.component.html

@ -15,6 +15,4 @@
limitations under the License.
-->
<ng-container *ngIf="markdownText$ | async as text;">
<tb-markdown [style]="style" [data]="text" lineNumbers (ready)="onMarkdownReady()" markdownClass="tb-help-markdown" (click)="markdownClick($event)"></tb-markdown>
</ng-container>
<tb-markdown [style]="style" [data]="markdownText$ | async" lineNumbers (ready)="onMarkdownReady()" markdownClass="tb-help-markdown" (click)="markdownClick($event)"></tb-markdown>

26
ui-ngx/src/app/shared/components/help-popup.component.html

@ -16,8 +16,9 @@
-->
<button #toggleHelpButton
*ngIf="!triggerText"
color="primary" mat-button mat-icon-button
*ngIf="!textMode"
mat-icon-button
color="primary"
class="tb-help-popup-button tb-mat-32"
type="button"
(click)="toggleHelp()"
@ -26,11 +27,16 @@
<mat-icon class="material-icons">{{popoverVisible ? 'help' : 'help_outline'}}</mat-icon>
<mat-spinner *ngIf="popoverVisible && !popoverReady" class="tb-help-popup-button-loading" mode="indeterminate" diameter="20" strokeWidth="2"></mat-spinner>
</button>
<span #toggleHelpSpan
*ngIf="triggerText"
class="tb-help-popup-span"
(click)="toggleHelp()"
[ngClass]="{'active': popoverVisible, 'ready': popoverReady}">
<span>{{triggerText}}</span>
<mat-progress-bar *ngIf="popoverVisible && !popoverReady" color="primary" mode="indeterminate"></mat-progress-bar>
</span>
<button #toggleHelpTextButton
*ngIf="textMode"
mat-button
color="primary"
class="tb-help-popup-text-button"
[ngClass]="{'mat-stroked-button': popoverVisible && popoverReady}"
(click)="toggleHelp()">
<ng-container *ngIf="triggerSafeHtml">
<span [style]="triggerStyle" [innerHTML]="triggerSafeHtml"></span>
</ng-container>
<mat-icon *ngIf="!popoverVisible || popoverReady" class="tb-mat-16">open_in_new</mat-icon>
<mat-spinner *ngIf="popoverVisible && !popoverReady" mode="indeterminate" diameter="16" strokeWidth="2"></mat-spinner>
</button>

56
ui-ngx/src/app/shared/components/help-popup.component.scss

@ -15,16 +15,14 @@
*/
.tb-help-popup-button {
position: relative;
}
.tb-help-popup-button-loading {
background: #fff;
border-radius: 50%;
width: 32px !important;
height: 32px !important;
position: absolute;
top: 0;
left: 0;
&.mat-progress-spinner {
.mat-progress-spinner {
position: absolute;
top: 0;
left: 0;
background: #fff;
border-radius: 50%;
width: 32px !important;
height: 32px !important;
svg {
top: 6px;
left: 6px;
@ -32,36 +30,20 @@
}
}
.tb-help-popup-span {
.tb-help-popup-text-button {
position: relative;
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
color: #2a7dec;
font-weight: 500;
&.active.ready {
text-decoration: underline;
padding: 0 2px 0 8px;
line-height: 28px;
&.mat-stroked-button {
padding: 0 1px 0 7px;
line-height: 26px;
}
&:hover {
&:not(.active) {
text-decoration: underline;
}
.mat-icon {
padding-left: 4px;
}
&:after {
.mat-progress-spinner {
display: inline-block;
font-family: "FontAwesome";
content: '\f08e';
padding-left: 5px;
font-size: 14px;
font-weight: 600;
}
.mat-progress-bar {
position: absolute;
height: 2px;
left: 0;
right: 0;
bottom: 0;
margin-left: 4px;
margin-right: 5px;
}
}

32
ui-ngx/src/app/shared/components/help-popup.component.ts

@ -17,15 +17,17 @@
import {
Component,
ElementRef,
Input,
Input, OnChanges,
OnDestroy,
Renderer2,
Renderer2, SimpleChanges,
ViewChild,
ViewContainerRef,
ViewEncapsulation
} from '@angular/core';
import { TbPopoverService } from '@shared/components/popover.service';
import { PopoverPlacement } from '@shared/components/popover.models';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { isDefinedAndNotNull } from '@core/utils';
@Component({
// tslint:disable-next-line:component-selector
@ -34,10 +36,10 @@ import { PopoverPlacement } from '@shared/components/popover.models';
styleUrls: ['./help-popup.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class HelpPopupComponent implements OnDestroy {
export class HelpPopupComponent implements OnChanges, OnDestroy {
@ViewChild('toggleHelpButton', {read: ElementRef, static: false}) toggleHelpButton: ElementRef;
@ViewChild('toggleHelpSpan', {read: ElementRef, static: false}) toggleHelpSpan: ElementRef;
@ViewChild('toggleHelpTextButton', {read: ElementRef, static: false}) toggleHelpTextButton: ElementRef;
// tslint:disable-next-line:no-input-rename
@Input('tb-help-popup') helpId: string;
@ -45,6 +47,9 @@ export class HelpPopupComponent implements OnDestroy {
// tslint:disable-next-line:no-input-rename
@Input('trigger-text') triggerText: string;
// tslint:disable-next-line:no-input-rename
@Input('trigger-style') triggerStyle: string;
// tslint:disable-next-line:no-input-rename
@Input('tb-help-popup-placement') helpPopupPlacement: PopoverPlacement;
@ -54,12 +59,27 @@ export class HelpPopupComponent implements OnDestroy {
popoverVisible = false;
popoverReady = true;
triggerSafeHtml: SafeHtml = null;
textMode = false;
constructor(private viewContainerRef: ViewContainerRef,
private element: ElementRef<HTMLElement>,
private sanitizer: DomSanitizer,
private renderer: Renderer2,
private popoverService: TbPopoverService) {}
private popoverService: TbPopoverService) {
}
ngOnChanges(changes: SimpleChanges): void {
if (isDefinedAndNotNull(this.triggerText)) {
this.triggerSafeHtml = this.sanitizer.bypassSecurityTrustHtml(this.triggerText);
} else {
this.triggerSafeHtml = null;
}
this.textMode = this.triggerSafeHtml != null;
}
toggleHelp() {
const trigger = this.triggerText ? this.toggleHelpSpan.nativeElement : this.toggleHelpButton.nativeElement;
const trigger = this.textMode ? this.toggleHelpTextButton.nativeElement : this.toggleHelpButton.nativeElement;
this.popoverService.toggleHelpPopover(trigger, this.renderer, this.viewContainerRef,
this.helpId,
'',

26
ui-ngx/src/app/shared/components/markdown.component.html

@ -0,0 +1,26 @@
<!--
Copyright © 2016-2021 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<ng-container #markdownContainer>
</ng-container>
<div *ngIf="error && !fallbackToPlainMarkdown" style="color: #f00; font-size: 14px;
line-height: 28px;
background: #efefef;">
{{error}}
</div>
<div #fallbackElement [fxShow]="error && fallbackToPlainMarkdown" class="tb-markdown-view" [ngClass]="markdownClass" [ngStyle]="style">
</div>

75
ui-ngx/src/app/shared/components/markdown.component.ts

@ -18,7 +18,7 @@ import {
ChangeDetectorRef,
Component,
ComponentFactory,
ComponentRef,
ComponentRef, ElementRef,
EventEmitter,
Inject,
Injector,
@ -33,20 +33,17 @@ import { MarkdownService, PrismPlugin } from 'ngx-markdown';
import { DynamicComponentFactoryService } from '@core/services/dynamic-component-factory.service';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { SHARED_MODULE_TOKEN } from '@shared/components/tokens';
import { isDefinedAndNotNull } from '@core/utils';
import { Observable, of, ReplaySubject } from 'rxjs';
@Component({
selector: 'tb-markdown',
template: '<ng-container #markdownContainer>' +
'</ng-container>' +
'<div *ngIf="error" style="color: #f00; font-size: 14px;' +
' line-height: 28px;' +
' background: #efefef;">' +
'{{error}}' +
'</div>'
templateUrl: './markdown.component.html'
})
export class TbMarkdownComponent implements OnChanges {
@ViewChild('markdownContainer', {read: ViewContainerRef, static: true}) markdownContainer: ViewContainerRef;
@ViewChild('fallbackElement', {static: true}) fallbackElement: ElementRef<HTMLElement>;
@Input() data: string | undefined;
@ -58,9 +55,14 @@ export class TbMarkdownComponent implements OnChanges {
get lineNumbers(): boolean { return this.lineNumbersValue; }
set lineNumbers(value: boolean) { this.lineNumbersValue = coerceBooleanProperty(value); }
@Input()
get fallbackToPlainMarkdown(): boolean { return this.fallbackToPlainMarkdownValue; }
set fallbackToPlainMarkdown(value: boolean) { this.fallbackToPlainMarkdownValue = coerceBooleanProperty(value); }
@Output() ready = new EventEmitter<void>();
private lineNumbersValue = false;
private fallbackToPlainMarkdownValue = false;
isMarkdownReady = false;
@ -76,14 +78,14 @@ export class TbMarkdownComponent implements OnChanges {
private dynamicComponentFactoryService: DynamicComponentFactoryService) {}
ngOnChanges(changes: SimpleChanges): void {
if (this.data) {
if (isDefinedAndNotNull(this.data)) {
this.render(this.data);
}
}
private render(markdown: string) {
let template = this.markdownService.compile(markdown, false);
template = this.sanitizeCurlyBraces(template);
const compiled = this.markdownService.compile(markdown, false);
let template = this.sanitizeCurlyBraces(compiled);
let markdownClass = 'tb-markdown-view';
if (this.markdownClass) {
markdownClass += ` ${this.markdownClass}`;
@ -91,6 +93,7 @@ export class TbMarkdownComponent implements OnChanges {
template = `<div [ngStyle]="style" class="${markdownClass}">${template}</div>`;
this.markdownContainer.clear();
const parent = this;
let readyObservable: Observable<void>;
this.dynamicComponentFactoryService.createDynamicComponentFactory(
class TbMarkdownInstance {
ngOnDestroy(): void {
@ -109,22 +112,40 @@ export class TbMarkdownComponent implements OnChanges {
this.tbMarkdownInstanceComponentRef.instance.style = this.style;
this.handlePlugins(this.tbMarkdownInstanceComponentRef.location.nativeElement);
this.markdownService.highlight(this.tbMarkdownInstanceComponentRef.location.nativeElement);
readyObservable = this.handleImages(this.tbMarkdownInstanceComponentRef.location.nativeElement);
this.error = null;
} catch (error) {
this.error = (error ? error + '' : 'Failed to render markdown!').replace(/\n/g, '<br>');
this.destroyMarkdownInstanceResources();
readyObservable = this.handleError(compiled, error);
}
this.cd.detectChanges();
this.ready.emit();
readyObservable.subscribe(() => {
this.ready.emit();
});
},
(error) => {
this.error = (error ? error + '' : 'Failed to render markdown!').replace(/\n/g, '<br>');
this.destroyMarkdownInstanceResources();
readyObservable = this.handleError(compiled, error);
this.cd.detectChanges();
this.ready.emit();
readyObservable.subscribe(() => {
this.ready.emit();
});
});
}
private handleError(template: string, error): Observable<void> {
this.error = (error ? error + '' : 'Failed to render markdown!').replace(/\n/g, '<br>');
this.destroyMarkdownInstanceResources();
if (this.fallbackToPlainMarkdownValue) {
this.markdownContainer.clear();
const element = this.fallbackElement.nativeElement;
element.innerHTML = template;
this.handlePlugins(element);
this.markdownService.highlight(element);
return this.handleImages(element);
} else {
return of(null);
}
}
private handlePlugins(element: HTMLElement): void {
if (this.lineNumbers) {
this.setPluginClass(element, PrismPlugin.LineNumbers);
@ -139,6 +160,26 @@ export class TbMarkdownComponent implements OnChanges {
}
}
private handleImages(element: HTMLElement): Observable<void> {
const imgs = $('img', element);
if (imgs.length) {
let totalImages = imgs.length;
const imagesLoadedSubject = new ReplaySubject<void>();
imgs.each((index, img) => {
$(img).one('load error', () => {
totalImages--;
if (totalImages === 0) {
imagesLoadedSubject.next();
imagesLoadedSubject.complete();
}
});
});
return imagesLoadedSubject.asObservable();
} else {
return of(null);
}
}
private sanitizeCurlyBraces(template: string): string {
return template.replace(/{/g, '&#123;').replace(/}/g, '&#125;');
}

26
ui-ngx/src/app/shared/components/marked-options.service.ts

@ -21,6 +21,7 @@ import { DOCUMENT } from '@angular/common';
import { WINDOW } from '@core/services/window.service';
const copyCodeBlock = '{:copy-code}';
const autoBlock = '{:auto}';
const targetBlankBlock = '{:target=&quot;_blank&quot;}';
// @dynamic
@ -48,13 +49,25 @@ export class MarkedOptionsService extends MarkedOptions {
this.renderer.code = (code: string, language: string | undefined, isEscaped: boolean) => {
if (code.endsWith(copyCodeBlock)) {
code = code.substring(0, code.length - copyCodeBlock.length);
const content = checkLineNumbers(this.renderer2.code(code, language, isEscaped), code);
const content = postProcessCodeContent(this.renderer2.code(code, language, isEscaped), code);
this.id++;
return this.wrapCopyCode(this.id, content, code);
} else {
return this.wrapDiv(checkLineNumbers(this.renderer2.code(code, language, isEscaped), code));
return this.wrapDiv(postProcessCodeContent(this.renderer2.code(code, language, isEscaped), code));
}
};
this.renderer.table = (header: string, body: string) => {
let autoLayout = false;
if (header.includes(autoBlock)) {
autoLayout = true;
header = header.replace(autoBlock, '');
}
let table = this.renderer2.table(header, body);
if (autoLayout) {
table = table.replace('<table', '<table class="auto"');
}
return table;
};
this.renderer.tablecell = (content: string, flags: {
header: boolean;
align: 'center' | 'left' | 'right' | null;
@ -151,10 +164,13 @@ export class MarkedOptionsService extends MarkedOptions {
}
}
function checkLineNumbers(content: string, code: string): string {
function postProcessCodeContent(content: string, code: string): string {
const lineCount = code.trim().split('\n').length;
let replacement;
if (lineCount < 2) {
content = content.replace('<pre>', '<pre class="no-line-numbers">');
replacement = '<pre ngNonBindable class="no-line-numbers">';
} else {
replacement = '<pre ngNonBindable>';
}
return content;
return content.replace('<pre>', replacement);
}

19
ui-ngx/src/assets/help/en_US/widget/action/custom_action_args.md

@ -0,0 +1,19 @@
<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.
</li>
<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
and data used by widget instance.
</li>
<li><b>entityId:</b> <code>string</code> - An optional string id of the target entity.
</li>
<li><b>entityName:</b> <code>string</code> - An optional string name of the target entity.
</li>
<li><b>additionalParams:</b> <code>{[key: string]: any}</code> - An optional key/value object holding additional entity parameters.
<span style="padding-left: 4px;"
tb-help-popup="widget/action/custom_additional_params"
tb-help-popup-placement="top"
[tb-help-popup-style]="{maxHeight: '50vh', maxWidth: '50vw'}"
trigger-text="Read more">
</span>
</li>
<li><b>entityLabel:</b> <code>string</code> - An optional string label of the target entity.
</li>

13
ui-ngx/src/assets/help/en_US/widget/action/custom_action_fn.md

@ -10,18 +10,7 @@ A JavaScript function performing custom action.
**Parameters:**
<ul>
<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.
</li>
<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
and data used by widget instance.
</li>
<li><b>entityId:</b> <code>string</code> - An optional string id of the target entity.
</li>
<li><b>entityName:</b> <code>string</code> - An optional string name of the target entity.
</li>
{% include widget/action/custom_additional_params %}
<li><b>entityLabel:</b> <code>string</code> - An optional string label of the target entity.
</li>
{% include widget/action/custom_action_args %}
</ul>
<div class="divider"></div>

97
ui-ngx/src/assets/help/en_US/widget/action/custom_additional_params.md

@ -1,46 +1,53 @@
<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:
<ul>
<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>:
<ul>
<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
<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
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.
</li>
</ul>
</li>
<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>:
<ul>
<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
<a href="https://github.com/thingsboard/thingsboard/blob/e264f7b8ddff05bda85c4833bf497f47f447496e/ui-ngx/src/app/shared/models/alarm.models.ts#L108" target="_blank">AlarmDataInfo</a> object
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.
</li>
</ul>
</li>
<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>:
<ul>
<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
<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
presenting <code>formattedTs</code> (a string value of formatted timestamp) and <br> timeseries values for each column declared in widget datasource configuration.
</li>
</ul>
</li>
<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>:
<ul>
<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
<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
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.
</li>
</ul>
</li>
<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>:
<ul>
<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
<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
containing <code>series</code> field with information about datasource and <br> data key of clicked pie slice.
</li>
</ul>
</li>
<li><i>All other widgets</i> - does not provide <b>additionalParams</b> value.
</li>
</ul>
#### Additional params object
<div class="divider"></div>
<br/>
<b>additionalParams:</b> <code>{[key: string]: any}</code>
An optional key/value object holding additional entity parameters depending on widget type and action source:
<ul>
<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>:
<ul>
<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
<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
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.
</li>
</ul>
</li>
<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>:
<ul>
<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
<a href="https://github.com/thingsboard/thingsboard/blob/e264f7b8ddff05bda85c4833bf497f47f447496e/ui-ngx/src/app/shared/models/alarm.models.ts#L108" target="_blank">AlarmDataInfo</a> object
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.
</li>
</ul>
</li>
<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>:
<ul>
<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
<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
presenting <code>formattedTs</code> (a string value of formatted timestamp) and <br> timeseries values for each column declared in widget datasource configuration.
</li>
</ul>
</li>
<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>:
<ul>
<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
<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
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.
</li>
</ul>
</li>
<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>:
<ul>
<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
<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
containing <code>series</code> field with information about datasource and <br> data key of clicked pie slice.
</li>
</ul>
</li>
<li><i>All other widgets</i> - does not provide <b>additionalParams</b> value.
</li>
</ul>

13
ui-ngx/src/assets/help/en_US/widget/action/custom_pretty_action_fn.md

@ -21,7 +21,14 @@ A JavaScript function performing custom action with defined HTML template to ren
</li>
<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).
</li>
{% include widget/action/custom_additional_params %}
<li><b>additionalParams:</b> <code>{[key: string]: any}</code> - An optional key/value object holding additional entity parameters.
<span style="padding-left: 4px;"
tb-help-popup="widget/action/custom_additional_params"
tb-help-popup-placement="top"
[tb-help-popup-style]="{maxHeight: '50vh', maxWidth: '50vw'}"
trigger-text="Read more">
</span>
</li>
<li><b>entityLabel:</b> <code>string</code> - An optional string label of the target entity.
</li>
</ul>
@ -38,6 +45,7 @@ A JavaScript function performing custom action with defined HTML template to ren
tb-help-popup="widget/action/examples/custom_pretty_create_dialog_js"
tb-help-popup-placement="top"
[tb-help-popup-style]="{maxHeight: '50vh', maxWidth: '50vw'}"
trigger-style="font-size: 16px;"
trigger-text="JavaScript function">
</div>
@ -47,6 +55,7 @@ A JavaScript function performing custom action with defined HTML template to ren
tb-help-popup="widget/action/examples/custom_pretty_create_dialog_html"
tb-help-popup-placement="top"
[tb-help-popup-style]="{maxHeight: '50vh', maxWidth: '50vw'}"
trigger-style="font-size: 16px;"
trigger-text="HTML code">
</div>
@ -58,6 +67,7 @@ A JavaScript function performing custom action with defined HTML template to ren
tb-help-popup="widget/action/examples/custom_pretty_edit_dialog_js"
tb-help-popup-placement="top"
[tb-help-popup-style]="{maxHeight: '50vh', maxWidth: '50vw'}"
trigger-style="font-size: 16px;"
trigger-text="JavaScript function">
</div>
@ -67,6 +77,7 @@ A JavaScript function performing custom action with defined HTML template to ren
tb-help-popup="widget/action/examples/custom_pretty_edit_dialog_html"
tb-help-popup-placement="top"
[tb-help-popup-style]="{maxHeight: '50vh', maxWidth: '50vw'}"
trigger-style="font-size: 16px;"
trigger-text="HTML code">
</div>

49
ui-ngx/src/assets/help/en_US/widget/action/mobile_get_location_fn.md

@ -0,0 +1,49 @@
#### Get location function
<div class="divider"></div>
<br/>
*function getLocation($event, widgetContext, entityId, entityName, additionalParams, entityLabel): [number, number] | Observable<[number, number]>*
A JavaScript function that should return location as array of two numbers (latitude, longitude) for further processing by mobile action.<br>
Usually location can be obtained from entity attributes/telemetry.
**Parameters:**
<ul>
{% include widget/action/custom_action_args %}
</ul>
**Returns:**
Latitude and longitude as array of two numbers or Observable of array of two numbers. For example ```[37.689, -122.433]```.
<div class="divider"></div>
##### Examples
* Return location from entity attributes:
```javascript
return getLocationFromEntityAttributes();
function getLocationFromEntityAttributes() {
if (entityId) {
return widgetContext.attributeService.getEntityAttributes(entityId, 'SERVER_SCOPE', ['latitude', 'longitude'])
.pipe(widgetContext.rxjs
.map(function(attributeData) {
var res = [0,0];
if (attributeData && attributeData.length === 2) {
res[0] = attributeData.filter(function (data) { return data.key === 'latitude'})[0].value;
res[1] = attributeData.filter(function (data) { return data.key === 'longitude'})[0].value;
}
return res;
}
)
);
} else {
return [0,0];
}
}
{:copy-code}
```

48
ui-ngx/src/assets/help/en_US/widget/action/mobile_get_phone_number_fn.md

@ -0,0 +1,48 @@
#### Get phone number function
<div class="divider"></div>
<br/>
*function getPhoneNumber($event, widgetContext, entityId, entityName, additionalParams, entityLabel): number | string | Observable&lt;number&gt; | Observable&lt;string&gt;*
A JavaScript function that should return phone number for further processing by mobile action.<br>
Usually phone number can be obtained from entity attributes/telemetry.
**Parameters:**
<ul>
{% include widget/action/custom_action_args %}
</ul>
**Returns:**
String or numeric value of phone number or Observable of string or numeric value. For example ```123456789```.
<div class="divider"></div>
##### Examples
* Return phone number from entity attributes:
```javascript
return getPhoneNumberFromEntityAttributes();
function getPhoneNumberFromEntityAttributes() {
if (entityId) {
return widgetContext.attributeService.getEntityAttributes(entityId, 'SERVER_SCOPE', ['phone'])
.pipe(widgetContext.rxjs
.map(function(attributeData) {
var res = 0;
if (attributeData && attributeData.length === 1) {
res = attributeData[0].value;
}
return res;
}
)
);
} else {
return 0;
}
}
{:copy-code}
```

31
ui-ngx/src/assets/help/en_US/widget/action/mobile_handle_empty_result_fn.md

@ -0,0 +1,31 @@
#### Handle empty result function
<div class="divider"></div>
<br/>
*function handleEmptyResult($event, widgetContext, entityId, entityName, additionalParams, entityLabel): void*
An optional JavaScript function to handle empty result.<br>Usually this happens when user cancels the action (for ex. by pressing phone back button).
**Parameters:**
<ul>
{% include widget/action/custom_action_args %}
</ul>
<div class="divider"></div>
##### Examples
* Display alert dialog with canceled action message:
```javascript
showEmptyResultDialog('Action was canceled!');
function showEmptyResultDialog(message) {
setTimeout(function() {
widgetContext.dialogs.alert('Empty result', message).subscribe();
}, 100);
}
{:copy-code}
```

33
ui-ngx/src/assets/help/en_US/widget/action/mobile_handle_error_fn.md

@ -0,0 +1,33 @@
#### Handle error function
<div class="divider"></div>
<br/>
*function handleError(error, $event, widgetContext, entityId, entityName, additionalParams, entityLabel): void*
An optional JavaScript function to handle error occurred while mobile action execution.
**Parameters:**
<ul>
<li><b>error:</b> <code>string</code> - error message.
</li>
{% include widget/action/custom_action_args %}
</ul>
<div class="divider"></div>
##### Examples
* Display alert dialog with error message:
```javascript
showErrorDialog('Failed to perform action', error);
function showErrorDialog(title, error) {
setTimeout(function() {
widgetContext.dialogs.alert(title, error).subscribe();
}, 100);
}
{:copy-code}
```

92
ui-ngx/src/assets/help/en_US/widget/action/mobile_process_image_fn.md

@ -0,0 +1,92 @@
#### Process image function
<div class="divider"></div>
<br/>
*function processImage(imageUrl, $event, widgetContext, entityId, entityName, additionalParams, entityLabel): void*
A JavaScript function to process image obtained as a result of mobile action (take photo, take image from gallery, etc.).
**Parameters:**
<ul>
<li><b>imageUrl:</b> <code>string</code> - An image URL in base64 data format.
</li>
{% include widget/action/custom_action_args %}
</ul>
<div class="divider"></div>
##### Examples
* Store image url data to entity attribute:
```javascript
saveEntityImageAttribute('image', imageUrl);
function saveEntityImageAttribute(attributeName, imageUrl) {
if (entityId) {
let attributes = [{
key: attributeName, value: imageUrl
}];
widgetContext.attributeService.saveEntityAttributes(entityId, "SERVER_SCOPE", attributes).subscribe(
function() {
widgetContext.showSuccessToast('Image attribute saved!');
},
function(error) {
widgetContext.dialogs.alert('Image attribute save failed', JSON.stringify(error));
}
);
}
}
{:copy-code}
```
* Display dialog with obtained image:
```javascript
showImageDialog('Image', imageUrl);
function showImageDialog(title, imageUrl) {
setTimeout(function() {
widgetContext.customDialog.customDialog(imageDialogTemplate, ImageDialogController, {imageUrl: imageUrl, title: title}).subscribe();
}, 100);
}
var imageDialogTemplate =
'<div aria-label="Image">' +
'<form #theForm="ngForm">' +
'<mat-toolbar fxLayout="row" color="primary">' +
'<h2>{{title}}</h2>' +
'<span fxFlex></span>' +
'<button mat-icon-button (click)="close()">' +
'<mat-icon>close</mat-icon>' +
'</button>' +
'</mat-toolbar>' +
'<div mat-dialog-content>' +
'<div class="mat-content mat-padding">' +
'<div fxLayout="column" fxFlex>' +
'<div style="padding-top: 20px;">' +
'<img [src]="imageUrl" style="height: 300px;"/>' +
'</div>' +
'</div>' +
'</div>' +
'</div>' +
'<div mat-dialog-actions fxLayout="row">' +
'<span fxFlex></span>' +
'<button mat-button (click)="close()" style="margin-right:20px;">Close</button>' +
'</div>' +
'</form>' +
'</div>';
function ImageDialogController(instance) {
let vm = instance;
vm.title = vm.data.title;
vm.imageUrl = vm.data.imageUrl;
vm.close = function ()
{
vm.dialogRef.close(null);
}
}
{:copy-code}
```

33
ui-ngx/src/assets/help/en_US/widget/action/mobile_process_launch_result_fn.md

@ -0,0 +1,33 @@
#### Process launch result function
<div class="divider"></div>
<br/>
*function processLaunchResult(launched, $event, widgetContext, entityId, entityName, additionalParams, entityLabel): void*
An optional JavaScript function to process result of attempt to launch external mobile application (for ex. map application or phone call application).
**Parameters:**
<ul>
<li><b>launched:</b> <code>boolean</code> - boolean value indicating if the external application was successfully launched.
{% include widget/action/custom_action_args %}
</ul>
<div class="divider"></div>
##### Examples
* Display alert dialog with external application launch status:
```javascript
showLaunchStatusDialog('Application', launched);
function showLaunchStatusDialog(title, status) {
setTimeout(function() {
widgetContext.dialogs.alert(title, status ? 'Successfully launched' : 'Failed to launch').subscribe();
}, 100);
}
{:copy-code}
```

59
ui-ngx/src/assets/help/en_US/widget/action/mobile_process_location_fn.md

@ -0,0 +1,59 @@
#### Process location function
<div class="divider"></div>
<br/>
*function processLocation(latitude, longitude, $event, widgetContext, entityId, entityName, additionalParams, entityLabel): void*
A JavaScript function to process current location of the phone.
**Parameters:**
<ul>
<li><b>latitude:</b> <code>number</code> - phone location latitude.
</li>
<li><b>longitude:</b> <code>number</code> - phone location longitude.
</li>
{% include widget/action/custom_action_args %}
</ul>
<div class="divider"></div>
##### Examples
* Display alert dialog with location data:
```javascript
showLocationDialog('Location', latitude, longitude);
function showLocationDialog(title, latitude, longitude) {
setTimeout(function() {
widgetContext.dialogs.alert(title, 'Latitude: '+latitude+'<br>Longitude: ' + longitude).subscribe();
}, 100);
}
{:copy-code}
```
* Store phone location to entity attributes:
```javascript
saveEntityLocationAttributes('latitude', 'longitude', latitude, longitude);
function saveEntityLocationAttributes(latitudeAttributeName, longitudeAttributeName, latitude, longitude) {
if (entityId) {
let attributes = [
{ key: latitudeAttributeName, value: latitude },
{ key: longitudeAttributeName, value: longitude }
];
widgetContext.attributeService.saveEntityAttributes(entityId, "SERVER_SCOPE", attributes).subscribe(
function() {
widgetContext.showSuccessToast('Location attributes saved!');
},
function(error) {
widgetContext.dialogs.alert('Location attributes save failed', JSON.stringify(error));
}
);
}
}
{:copy-code}
```

76
ui-ngx/src/assets/help/en_US/widget/action/mobile_process_qr_code_fn.md

@ -0,0 +1,76 @@
#### Process QR code function
<div class="divider"></div>
<br/>
*function processQrCode(code, format, $event, widgetContext, entityId, entityName, additionalParams, entityLabel): void*
A JavaScript function to process result of barcode scanning.
**Parameters:**
<ul>
<li><b>code:</b> <code>string</code> - A string value of scanned barcode.
</li>
<li><b>format:</b> <code>string</code> - barcode format. See <a href="https://github.com/juliuscanute/qr_code_scanner/blob/c89f1eaddb94cca705d7e602a0c326e271680bf4/lib/src/types/barcode_format.dart#L1" target="_blank">BarcodeFormat</a> enum for possible values.
</li>
{% include widget/action/custom_action_args %}
</ul>
<div class="divider"></div>
##### Examples
* Display alert dialog with scanned barcode:
```javascript
showQrCodeDialog('Bar Code', code, format);
function showQrCodeDialog(title, code, format) {
setTimeout(function() {
widgetContext.dialogs.alert(title, 'Code: ['+code+']<br>Format: ' + format).subscribe();
}, 100);
}
{:copy-code}
```
* Parse code as a device claiming info (in this case ```{deviceName: string, secretKey: string}```)<br>and then claim device (see [Claiming devices{:target="_blank"}](${baseUrl}/docs/user-guide/claiming-devices/) for details):
```javascript
var $scope = widgetContext.$scope;
var $injector = $scope.$injector;
var $translate = $injector.get(widgetContext.servicesMap.get('translate'));
var deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));
var deviceNotFound = $translate.instant('widgets.input-widgets.claim-not-found');
var failedClaimDevice = $translate.instant('widgets.input-widgets.claim-failed');
var claimDeviceInfo = JSON.parse(code);
var deviceName = claimDeviceInfo.deviceName;
var secretKey = claimDeviceInfo.secretKey;
var claimRequest = {
secretKey: secretKey
};
deviceService.claimDevice(deviceName, claimRequest, { ignoreErrors: true }).subscribe(
function (data) {
widgetContext.showSuccessToast('Device \'' + deviceName + '\' successfully claimed!');
widgetContext.updateAliases();
},
function (error) {
if(error.status == 404) {
widgetContext.showErrorToast(deviceNotFound);
} else {
if (error.status !== 400 && error.error && error.error.message) {
showDialog('Failed to claim device', error.error.message);
} else {
widgetContext.showErrorToast(failedClaimDevice);
}
}
}
);
function showDialog(title, error) {
setTimeout(function() {
widgetContext.dialogs.alert(title, error).subscribe();
}, 100);
}
{:copy-code}
```

147
ui-ngx/src/assets/help/en_US/widget/editor/examples/alarm_widget.md

@ -0,0 +1,147 @@
#### Sample Alarm widget
<div class="divider"></div>
<br/>
In the **Widgets Bundle** view, click the big “+” button at the bottom-right part of the screen and then click the “Create new widget type” button.<br>
Click the **Alarm Widget** button on the **Select widget type** popup.<br>
The **Widget Editor** will be opened, pre-populated with the content of the default **Alarm** template widget.
- Replace content of the CSS tab in "Resources" section with the following one:
```css
.my-alarm-table th {
text-align: left;
}
{:copy-code}
```
- Put the following HTML code inside the HTML tab of "Resources" section:
```html
<div fxFlex fxLayout="column" style="height: 100%;">
<div>My first Alarm widget.</div>
<table class="my-alarm-table" style="width: 100%;">
<thead>
<tr>
<th *ngFor="let dataKey of alarmSource?.dataKeys">{{dataKey.label}}</th>
<tr>
</thead>
<tbody>
<tr *ngFor="let alarm of alarms">
<td *ngFor="let dataKey of alarmSource?.dataKeys"
[ngStyle]="getAlarmCellStyle(alarm, dataKey)">
{{getAlarmValue(alarm, dataKey)}}
</td>
</tr>
</tbody>
</table>
</div>
{:copy-code}
```
- Put the following JSON content inside the "Settings schema" tab of **Settings schema section**:
```json
{
"schema": {
"type": "object",
"title": "AlarmTableSettings",
"properties": {
"alarmSeverityColorFunction": {
"title": "Alarm severity color function: f(severity)",
"type": "string",
"default": "if(severity == 'CRITICAL') {return 'red';} else if (severity == 'MAJOR') {return 'orange';} else return 'green'; "
}
},
"required": []
},
"form": [
{
"key": "alarmSeverityColorFunction",
"type": "javascript"
}
]
}
{:copy-code}
```
- Put the following JavaScript code inside the "JavaScript" section:
```javascript
self.onInit = function() {
var pageLink = self.ctx.pageLink();
pageLink.typeList = self.ctx.widgetConfig.alarmTypeList;
pageLink.statusList = self.ctx.widgetConfig.alarmStatusList;
pageLink.severityList = self.ctx.widgetConfig.alarmSeverityList;
pageLink.searchPropagatedAlarms = self.ctx.widgetConfig.searchPropagatedAlarms;
self.ctx.defaultSubscription.subscribeForAlarms(pageLink, null);
self.ctx.$scope.alarmSource = self.ctx.defaultSubscription.alarmSource;
var alarmSeverityColorFunctionBody = self.ctx.settings.alarmSeverityColorFunction;
if (typeof alarmSeverityColorFunctionBody === 'undefined' || !alarmSeverityColorFunctionBody.length) {
alarmSeverityColorFunctionBody = "if(severity == 'CRITICAL') {return 'red';} else if (severity == 'MAJOR') {return 'orange';} else return 'green';";
}
var alarmSeverityColorFunction = null;
try {
alarmSeverityColorFunction = new Function('severity', alarmSeverityColorFunctionBody);
} catch (e) {
alarmSeverityColorFunction = null;
}
self.ctx.$scope.getAlarmValue = function(alarm, dataKey) {
var alarmKey = dataKey.name;
if (alarmKey === 'originator') {
alarmKey = 'originatorName';
}
var value = alarm[alarmKey];
if (alarmKey === 'createdTime') {
return self.ctx.date.transform(value, 'yyyy-MM-dd HH:mm:ss');
} else {
return value;
}
}
self.ctx.$scope.getAlarmCellStyle = function(alarm, dataKey) {
var alarmKey = dataKey.name;
if (alarmKey === 'severity' && alarmSeverityColorFunction) {
var severity = alarm[alarmKey];
var color = alarmSeverityColorFunction(severity);
return {
color: color
};
}
return {};
}
}
self.onDataUpdated = function() {
self.ctx.$scope.alarms = self.ctx.defaultSubscription.alarms.data;
self.ctx.detectChanges();
}
{:copy-code}
```
- Click the **Run** button on the **Widget Editor Toolbar** in order to see the result in **Widget preview** section.
![image](${baseUrl}/images/user-guide/contribution/widgets/alarm-widget-sample.png)
In this example, the **alarmSource** and **alarms** properties of <span trigger-style="fontSize: 16px;" trigger-text="<b>subscription</b>" tb-help-popup="widget/editor/widget_js_subscription_object"></span> are assigned to **$scope** and become accessible within HTML template.
Inside the HTML, a special [***ngFor**{:target="_blank"}](https://angular.io/api/common/NgForOf) structural angular directive is used in order to iterate over available alarm **dataKeys** of **alarmSource** and render corresponding columns.
The table rows are rendered by iterating over **alarms** array and corresponding cells rendered by iterating over **dataKeys**.
The function **getAlarmValue** is fetching alarm value and formatting **createdTime** alarm property using a [DatePipe{:target="_blank"}](https://angular.io/api/common/DatePipe) angular pipe accessible via **date** property of **ctx**.
The function **getAlarmCellStyle** is used to assign custom cell styles for each alarm cell.<br>In this example, we introduced new settings property called **alarmSeverityColorFunction** that contains function body returning color depending on alarm severity.
Inside the **getAlarmCellStyle** function there is corresponding invocation of **alarmSeverityColorFunction** with severity value in order to get color for alarm severity cell.
Note that in this code **onDataUpdated** function is implemented in order to update **alarms** property with latest alarms from subscription and invoke change detection using **detectChanges()** function.
<br/>
<br/>

67
ui-ngx/src/assets/help/en_US/widget/editor/examples/ext_latest_values_example.md

@ -0,0 +1,67 @@
#### Latest Values widget Example with gauge.js library
<div class="divider"></div>
<br/>
In this example, **Latest Values** gauge widget will be created using external [gauge.js{:target="_blank"}](http://bernii.github.io/gauge.js/) library.
In the **Widgets Bundle** view, click the big “+” button at the bottom-right part of the screen, then click the “Create new widget type” button.<br>
Click the **Latest Values** button on the **Select widget type** popup.<br>
The **Widget Editor** will be opened, pre-populated with the content of default **Latest Values** template widget.
- Open **Resources** tab and click "Add" then insert the following link:
```
https://bernii.github.io/gauge.js/dist/gauge.min.js
{:copy-code}
```
- Clear content of the CSS tab of "Resources" section.
- Put the following HTML code inside the HTML tab of "Resources" section:
```html
<canvas id="my-gauge"></canvas>
{:copy-code}
```
- Put the following JavaScript code inside the "JavaScript" section:
```javascript
var canvasElement;
var gauge;
self.onInit = function() {
canvasElement = $('#my-gauge', self.ctx.$container)[0];
gauge = new Gauge(canvasElement);
gauge.minValue = -1000;
gauge.maxValue = 1000;
gauge.animationSpeed = 16;
self.onResize();
}
self.onResize = function() {
canvasElement.width = self.ctx.width;
canvasElement.height = self.ctx.height;
gauge.update(true);
gauge.render();
}
self.onDataUpdated = function() {
if (self.ctx.defaultSubscription.data[0].data.length) {
var value = self.ctx.defaultSubscription.data[0].data[0][1];
gauge.set(value);
}
}
{:copy-code}
```
- Click the **Run** button on the **Widget Editor Toolbar** in order to see the result in **Widget preview** section.
![image](${baseUrl}/images/user-guide/contribution/widgets/external-js-widget-sample.png)
In this example, the external JS library API was used that becomes available after injecting the corresponding URL in **Resources** section.
The value displayed was obtained from <span trigger-style="fontSize: 16px;" trigger-text="<b>subscription</b>" tb-help-popup="widget/editor/widget_js_subscription_object"></span> **data** property for the first dataKey.
<br/>
<br/>

112
ui-ngx/src/assets/help/en_US/widget/editor/examples/ext_timeseries_example.md

@ -0,0 +1,112 @@
#### Time-Series widget Example with Chart.js library
<div class="divider"></div>
<br/>
In this example, **Time-Series** line chart widget will be created using external [Chart.js{:target="_blank"}](https://www.chartjs.org/) library.
In the **Widgets Bundle** view, click the big “+” button at the bottom-right part of the screen, then click the “Create new widget type” button.<br>
Click the **Time-Series** button on the **Select widget type** popup.<br>
The **Widget Editor** will be opened, pre-populated with the content of default **Time-Series** template widget.
- Open **Resources** tab and click "Add" then insert the following link:
```
https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.min.js
{:copy-code}
```
- Clear content of the CSS tab of "Resources" section.
- Put the following HTML code inside the HTML tab of "Resources" section:
```html
<canvas id="myChart"></canvas>
{:copy-code}
```
- Put the following JavaScript code inside the "JavaScript" section:
```javascript
var myChart;
self.onInit = function() {
var chartData = {
datasets: []
};
for (var i=0; i < self.ctx.data.length; i++) {
var dataKey = self.ctx.data[i].dataKey;
var dataset = {
label: dataKey.label,
data: [],
borderColor: dataKey.color,
fill: false
};
chartData.datasets.push(dataset);
}
var options = {
maintainAspectRatio: false,
legend: {
display: false
},
scales: {
xAxes: [{
type: 'time',
ticks: {
maxRotation: 0,
autoSkipPadding: 30
}
}]
}
};
var canvasElement = $('#myChart', self.ctx.$container)[0];
var canvasCtx = canvasElement.getContext('2d');
myChart = new Chart(canvasCtx, {
type: 'line',
data: chartData,
options: options
});
self.onResize();
}
self.onResize = function() {
myChart.resize();
}
self.onDataUpdated = function() {
for (var i = 0; i < self.ctx.data.length; i++) {
var datasourceData = self.ctx.data[i];
var dataSet = datasourceData.data;
myChart.data.datasets[i].data.length = 0;
var data = myChart.data.datasets[i].data;
for (var d = 0; d < dataSet.length; d++) {
var tsValuePair = dataSet[d];
var ts = tsValuePair[0];
var value = tsValuePair[1];
data.push({t: ts, y: value});
}
}
myChart.options.scales.xAxes[0].ticks.min = self.ctx.timeWindow.minTime;
myChart.options.scales.xAxes[0].ticks.max = self.ctx.timeWindow.maxTime;
myChart.update();
}
{:copy-code}
```
- Click the **Run** button on the **Widget Editor Toolbar** in order to see the result in **Widget preview** section.
![image](${baseUrl}/images/user-guide/contribution/widgets/external-js-timeseries-widget-sample.png)
In this example, the external JS library API was used that becomes available after injecting the corresponding URL in **Resources** section.
Initially chart datasets prepared using configured dataKeys from **data** property of **ctx**.
In the **onDataUpdated** function datasources data converted to Chart.js line chart format and pushed to chart datasets.
Please note that xAxis (time axis) is limited to current timewindow bounds obtained from **timeWindow** property of **ctx**.
<br/>
<br/>

47
ui-ngx/src/assets/help/en_US/widget/editor/examples/latest_values_widget.md

@ -0,0 +1,47 @@
#### Sample Latest Values widget
<div class="divider"></div>
<br/>
In the **Widgets Bundle** view, click the big “+” button at the bottom-right part of the screen and then click the “Create new widget type” button.<br>
Click the **Latest Values** button on the **Select widget type** popup.<br>
The **Widget Editor** will open, pre-populated with the content of the default **Latest Values** template widget.
- Clear content of the CSS tab of "Resources" section.
- Put the following HTML code inside the HTML tab of "Resources" section:
```html
<div fxFlex fxLayout="column" style="height: 100%;" fxLayoutAlign="center stretch">
<div>My first latest values widget.</div>
<div fxFlex fxLayout="row" *ngFor="let dataKeyData of data" fxLayoutAlign="space-around center">
<div>{{dataKeyData.dataKey.label}}:</div>
<div>{{(dataKeyData.data[0] && dataKeyData.data[0][0]) | date : 'yyyy-MM-dd HH:mm:ss' }}</div>
<div>{{dataKeyData.data[0] && dataKeyData.data[0][1]}}</div>
</div>
</div>
{:copy-code}
```
- Put the following JavaScript code inside the "JavaScript" section:
```javascript
self.onInit = function() {
self.ctx.$scope.data = self.ctx.defaultSubscription.data;
}
self.onDataUpdated = function() {
self.ctx.detectChanges();
}
{:copy-code}
```
- Click the **Run** button on the **Widget Editor Toolbar** in order to see the result in **Widget preview** section.
![image](${baseUrl}/images/user-guide/contribution/widgets/latest-values-widget-sample.png)
In this example, the **data** property of <span trigger-style="fontSize: 16px;" trigger-text="<b>subscription</b>" tb-help-popup="widget/editor/widget_js_subscription_object"></span> is assigned to the **$scope** and becomes accessible within the HTML template.
Inside the HTML, a special [***ngFor**{:target="_blank"}](https://angular.io/api/common/NgForOf) structural angular directive is used in order to iterate over available dataKeys & datapoints then render latest values with their corresponding timestamps.
<br/>
<br/>

187
ui-ngx/src/assets/help/en_US/widget/editor/examples/rpc_widget.md

@ -0,0 +1,187 @@
#### Sample RPC (Control) widget
<div class="divider"></div>
<br/>
In the **Widgets Bundle** view, click the big “+” button at the bottom-right part of the screen and then click the “Create new widget type” button.<br>
Click the **Control Widget** button on the **Select widget type** popup.<br>
The **Widget Editor** will open, pre-populated with default **Control** template widget content.
- Clear content of the CSS tab of "Resources" section.
- Put the following HTML code inside the HTML tab of "Resources" section:
```html
<form #rpcForm="ngForm" (submit)="sendCommand()">
<div class="mat-content mat-padding" fxLayout="column">
<mat-form-field class="mat-block">
<mat-label>RPC method</mat-label>
<input matInput required name="rpcMethod" #rpcMethodField="ngModel" [(ngModel)]="rpcMethod"/>
<mat-error *ngIf="rpcMethodField.hasError('required')">
RPC method name is required.
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label>RPC params</mat-label>
<input matInput required name="rpcParams" #rpcParamsField="ngModel" [(ngModel)]="rpcParams"/>
<mat-error *ngIf="rpcParamsField.hasError('required')">
RPC params is required.
</mat-error>
</mat-form-field>
<button [disabled]="rpcForm.invalid || !rpcForm.dirty" mat-raised-button color="primary" type="submit" >
Send RPC command
</button>
<div>
<label>RPC command response</label>
<div style="width: 100%; height: 100px; border: solid 2px gray" [innerHTML]="rpcCommandResponse">
</div>
</div>
</div>
</form>
{:copy-code}
```
- Put the following JSON content inside the "Settings schema" tab of **Settings schema section**:
```json
{
"schema": {
"type": "object",
"title": "Settings",
"properties": {
"oneWayElseTwoWay": {
"title": "Is One Way Command",
"type": "boolean",
"default": true
},
"requestTimeout": {
"title": "RPC request timeout",
"type": "number",
"default": 500
}
},
"required": []
},
"form": [
"oneWayElseTwoWay",
"requestTimeout"
]
}
{:copy-code}
```
- Put the following JavaScript code inside the "JavaScript" section:
```javascript
self.onInit = function() {
self.ctx.$scope.sendCommand = function() {
var rpcMethod = self.ctx.$scope.rpcMethod;
var rpcParams = self.ctx.$scope.rpcParams;
var timeout = self.ctx.settings.requestTimeout;
var oneWayElseTwoWay = self.ctx.settings.oneWayElseTwoWay ? true : false;
var commandObservable;
if (oneWayElseTwoWay) {
commandObservable = self.ctx.controlApi.sendOneWayCommand(rpcMethod, rpcParams, timeout);
} else {
commandObservable = self.ctx.controlApi.sendTwoWayCommand(rpcMethod, rpcParams, timeout);
}
commandObservable.subscribe(
function (response) {
if (oneWayElseTwoWay) {
self.ctx.$scope.rpcCommandResponse = "Command was successfully received by device.<br> No response body because of one way command mode.";
} else {
self.ctx.$scope.rpcCommandResponse = "Response from device:<br>";
self.ctx.$scope.rpcCommandResponse += JSON.stringify(response, undefined, 2);
}
self.ctx.detectChanges();
},
function (rejection) {
self.ctx.$scope.rpcCommandResponse = "Failed to send command to the device:<br>"
self.ctx.$scope.rpcCommandResponse += "Status: " + rejection.status + "<br>";
self.ctx.$scope.rpcCommandResponse += "Status text: '" + rejection.statusText + "'";
self.ctx.detectChanges();
}
);
}
}
{:copy-code}
```
- Fill **Widget title** field with widget type name, for ex. "My first control widget".
- Click the **Run** button on the **Widget Editor Toolbar** in order to see the result in **Widget preview** section.
- Click dashboard edit button on the preview section to change the size of the resulting widget. Then click dashboard apply button. The final widget should look like the image below.
![image](${baseUrl}/images/user-guide/contribution/widgets/control-widget-sample.png)
- Click the **Save** button on the **Widget Editor Toolbar** to save widget type.
To test how this widget performs RPC commands, we will need to place it in a dashboard then bind it to a device working with RPC commands. To do this, perform the following steps:
- Login as Tenant administrator.
- Navigate to **Devices** and create new device with some name, for ex. "My RPC Device".
- Open device details and click "Copy Access Token" button to copy device access token to clipboard.
- Download [mqtt-js-rpc-from-server.sh{:target="_blank"}](${baseUrl}/docs/reference/resources/mqtt-js-rpc-from-server.sh) and [mqtt-js-rpc-from-server.js{:target="_blank"}](${baseUrl}/docs/reference/resources/mqtt-js-rpc-from-server.js). Place these files in a folder.
Edit **mqtt-js-rpc-from-server.sh** - replace **$ACCESS_TOKEN** with your device access token from the clipboard. And install mqtt client library.
- Run **mqtt-js-rpc-from-server.sh** script. You should see a "connected" message in the console.
- Navigate to **Dashboards** and create a new dashboard with some name, for ex. "My first control dashboard". Open this dashboard.
- Click dashboard "edit" button. In the dashboard edit mode, click the "Entity aliases" button located on the dashboard toolbar.
![image](${baseUrl}/images/user-guide/contribution/widgets/dashboard-toolbar-entity-aliases.png)
- Inside **Entity aliases** popup click "Add alias".
- Fill "Alias name" field, for ex. "My RPC Device Alias".
- Select "Entity list" in "Filter type" field.
- Choose "Device" in "Type" field.
- Select your device in "Entity list" field. In this example "My RPC Device".
![image](${baseUrl}/images/user-guide/contribution/widgets/add-rpc-device-alias.png)
- Click "Add" and then "Save" in **Entity aliases**.
- Click dashboard "+" button then click "Create new widget" button.
![image](${baseUrl}/images/user-guide/contribution/widgets/dashboard-create-new-widget-button.png)
- Then select **Widget Bundle** where your RPC widget was saved. Select "Control widget" tab.
- Click your widget. In this example, "My first control widget".
- From **Add Widget** popup, select your device alias in **Target device** section. In this example "My RPC Device Alias".
- Click **Add**. Your Control widget will appear in the dashboard. Click dashboard **Apply changes** button to save dashboard and leave editing mode.
- Fill **RPC method** field with RPC method name. For ex. "TestMethod".
- Fill **RPC params** field with RPC params. For ex. "{ param1: "value1" }".
- Click **Send RPC command** button. You should see the following response in the widget.
![image](${baseUrl}/images/user-guide/contribution/widgets/control-widget-sample-response-one-way.png)
The following output should be printed in the device console:
```bash
request.topic: v1/devices/me/rpc/request/0
request.body: {"method":"TestMethod","params":"{ param1: \"value1\" }"}
```
In order to test "Two way" RPC command mode, we need to change the corresponding widget settings property. To do this, perform the following steps:
- Click dashboard "edit" button. In dashboard edit mode, click **Edit widget** button located in the header of Control widget.
- In the widget details, view select "Advanced" tab and uncheck "Is One Way Command" checkbox.
![image](${baseUrl}/images/user-guide/contribution/widgets/control-widget-sample-settings.png)
- Click **Apply changes** button on the widget details header. Close details and click dashboard **Apply changes** button.
- Fill widget fields with RPC method name and params like in previous steps.
Click **Send RPC command** button. You should see the following response in the widget.
![image](${baseUrl}/images/user-guide/contribution/widgets/control-widget-sample-response-two-way.png)
- stop **mqtt-js-rpc-from-server.sh** script.
Click **Send RPC command** button. You should see the following response in the widget.
![image](${baseUrl}/images/user-guide/contribution/widgets/control-widget-sample-response-timeout.png)
In this example, **controlApi** is used to send RPC commands. Additionally, custom widget settings were introduced in order to configure RPC command mode and RPC request timeout.
The response from the device is handled by **commandObservable**. It has success and failed callbacks with corresponding response, or rejection objects containing information about request execution result.
<br>
<br>

72
ui-ngx/src/assets/help/en_US/widget/editor/examples/static_widget.md

@ -0,0 +1,72 @@
#### Sample Static widget
<div class="divider"></div>
<br/>
In the **Widgets Bundle** view, click the big “+” button at the bottom-right part of the screen and then click the “Create new widget type” button.<br>
Click the **Static Widget** button on the **Select widget type** popup.<br>
The **Widget Editor** will be opened pre-populated with the content of default **Static** template widget.
- Put the following HTML code inside the HTML tab of "Resources" section:
```html
<div fxFlex fxLayout="column" style="height: 100%;" fxLayoutAlign="space-around stretch">
<h3 style="text-align: center;">My first static widget.</h3>
<button mat-raised-button color="primary" (click)="showAlert()">Click me</button>
</div>
{:copy-code}
```
- Put the following JSON content inside the "Settings schema" tab of **Settings schema section**:
```json
{
"schema": {
"type": "object",
"title": "Settings",
"properties": {
"alertContent": {
"title": "Alert content",
"type": "string",
"default": "Content derived from alertContent property of widget settings."
}
}
},
"form": [
"alertContent"
]
}
{:copy-code}
```
- Put the following JavaScript code inside the "JavaScript" section:
```javascript
self.onInit = function() {
self.ctx.$scope.showAlert = function() {
var alertContent = self.ctx.settings.alertContent;
if (!alertContent) {
alertContent = "Content derived from alertContent property of widget settings.";
}
window.alert(alertContent);
};
}
{:copy-code}
```
- Click the **Run** button on the **Widget Editor Toolbar** to see the resulting **Widget preview** section.
![image](${baseUrl}/images/user-guide/contribution/widgets/static-widget-sample.png)
This is just a static HTML widget. There is no subscription data and no special widget API was used.
Only custom **showAlert** function was implemented showing an alert with the content of **alertContent** property of widget settings.
You can switch to dashboard edit mode in **Widget preview** section and change value of **alertContent** by changing widget settings in the "Advanced" tab of widget details.
Then you can see that the new alert content is displayed.
<br/>
<br/>

93
ui-ngx/src/assets/help/en_US/widget/editor/examples/timeseries_widget.md

@ -0,0 +1,93 @@
#### Sample Time-Series widget
<div class="divider"></div>
<br/>
In the **Widgets Bundle** view, click the big “+” button at the bottom-right part of the screen, then click the “Create new widget type” button.<br>
Click the **Time-Series** button on the **Select widget type** popup.<br>
The **Widget Editor** will open, pre-populated with default **Time-Series** template widget content.
- Replace content of the CSS tab in "Resources" section with the following one:
```css
.my-data-table th {
text-align: left;
}
{:copy-code}
```
- Put the following HTML code inside the HTML tab of "Resources" section:
```html
<mat-tab-group style="height: 100%;">
<mat-tab *ngFor="let datasource of datasources; let $dsIndex = index" label="{{datasource.name}}">
<table class="my-data-table" style="width: 100%;">
<thead>
<tr>
<th>Timestamp</th>
<th *ngFor="let dataKeyData of datasourceData[$dsIndex]">{{dataKeyData.dataKey.label}}</th>
<tr>
</thead>
<tbody>
<tr *ngFor="let data of datasourceData[$dsIndex][0].data; let $dataIndex = index">
<td>{{data[0] | date : 'yyyy-MM-dd HH:mm:ss'}}</td>
<td *ngFor="let dataKeyData of datasourceData[$dsIndex]">{{dataKeyData.data[$dataIndex] && dataKeyData.data[$dataIndex][1]}}</td>
</tr>
</tbody>
</table>
</mat-tab>
</mat-tab-group>
{:copy-code}
```
- Put the following JavaScript code inside the "JavaScript" section:
```javascript
self.onInit = function() {
self.ctx.widgetTitle = 'My first Time-Series widget';
self.ctx.$scope.datasources = self.ctx.defaultSubscription.datasources;
self.ctx.$scope.data = self.ctx.defaultSubscription.data;
self.ctx.$scope.datasourceData = [];
var currentDatasource = null;
var currentDatasourceIndex = -1;
for (var i=0;i<self.ctx.$scope.data.length;i++) {
var dataKeyData = self.ctx.$scope.data[i];
if (dataKeyData.datasource != currentDatasource) {
currentDatasource = dataKeyData.datasource
currentDatasourceIndex++;
self.ctx.$scope.datasourceData[currentDatasourceIndex] = [];
}
self.ctx.$scope.datasourceData[currentDatasourceIndex].push(dataKeyData);
}
self.ctx.updateWidgetParams();
}
self.onDataUpdated = function() {
self.ctx.detectChanges();
}
{:copy-code}
```
- Click the **Run** button on the **Widget Editor Toolbar** in order to see the result in **Widget preview** section.
![image](${baseUrl}/images/user-guide/contribution/widgets/timeseries-widget-sample.png)
In this example, the <span trigger-style="fontSize: 16px;" trigger-text="<b>subscription</b>" tb-help-popup="widget/editor/widget_js_subscription_object"></span> **datasources** and **data** properties are assigned to **$scope** and become accessible within the HTML template.
The **$scope.datasourceData** property is introduced to map datasource specific dataKeys data by datasource index for flexible access within the HTML template.
Inside the HTML, a special [***ngFor**{:target="_blank"}](https://angular.io/api/common/NgForOf) structural angular directive is used in order to iterate over available datasources and render corresponding tabs.
Inside each tab, the table is rendered using dataKeys obtained from **datasourceData** scope property accessed by datasource index.<br>
Each table renders columns by iterating over all **dataKeyData** objects and renders all available datapoints by iterating over **data** array of each **dataKeyData** to render timestamps and values.
Note that in this code, **onDataUpdated** function is implemented with a call to **detectChanges** function necessary to perform new change detection cycle when new data is received.
<br/>
<br/>

17
ui-ngx/src/assets/help/en_US/widget/editor/widget_js_action_sources_object.md

@ -0,0 +1,17 @@
#### Action sources object
<div class="divider"></div>
<br/>
Map describing available widget action sources ([WidgetActionSource{:target="_blank"}](https://github.com/thingsboard/thingsboard/blob/2627fe51d491055d4140f16617ed543f7f5bd8f6/ui-ngx/src/app/shared/models/widget.models.ts#L121)) to which user actions can be assigned. It has the following structure:
```javascript
return {
'headerButton': { // Action source Id (unique action source identificator)
name: 'widget-action.header-button', // Display name of action source, used in widget settings ('Actions' tab).
value: 'headerButton', // Action source Id
multiple: true // Boolean property indicating if this action source supports multiple action definitions
// (for ex. multiple buttons in one cell, or only one action can by assigned on table row click.)
}
};
```

62
ui-ngx/src/assets/help/en_US/widget/editor/widget_js_existing_code.md

@ -0,0 +1,62 @@
#### Using existing JavaScript/Typescript code
<div class="divider"></div>
<br/>
Another approach of creating widgets is to use existing bundled JavaScript/Typescript code.
In this case, you can create own TypeScript class or Angular component and bundle it into the ThingsBoard UI code.
In order to make this code accessible within the widget, you need to register corresponding Angular module or inject TypeScript class to a global variable (for ex. window object).
Some ThingsBoard widgets already use this approach. Take a look at the [widget-component-service.ts{:target="_blank"}](https://github.com/thingsboard/thingsboard/blob/2627fe51d491055d4140f16617ed543f7f5bd8f6/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts#L140)
or [widget-components.module.ts{:target="_blank"}](https://github.com/thingsboard/thingsboard/blob/2627fe51d491055d4140f16617ed543f7f5bd8f6/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts#L50). <br>
Here you can find how some bundled classes or components are registered for later use in ThingsBoard widgets.
For example "Timeseries - Flot" widget (from "Charts" Widgets Bundle) uses [**TbFlot**{:target="_blank"}](https://github.com/thingsboard/thingsboard/blob/2627fe51d491055d4140f16617ed543f7f5bd8f6/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.ts#L73) TypeScript class which is injected as window property inside **widget-component-service.ts**:
```typescript
...
const widgetModulesTasks: Observable<any>[] = [];
...
widgetModulesTasks.push(from(import('@home/components/widget/lib/flot-widget')).pipe(
tap((mod) => {
(window as any).TbFlot = mod.TbFlot;
}))
);
...
```
Another example is "Timeseries table" widget (from "Cards" Widgets Bundle) that uses Angular component [**tb-timeseries-table-widget**{:target="_blank"}](https://github.com/thingsboard/thingsboard/blob/2627fe51d491055d4140f16617ed543f7f5bd8f6/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts#L107)<br>which is registered as dependency of **WidgetComponentsModule** Angular module inside **widget-components.module.ts**.
Thereby this component becomes available for use inside the widget template HTML.
```typescript
...
import { TimeseriesTableWidgetComponent } from '@home/components/widget/lib/timeseries-table-widget.component';
...
@NgModule({
declarations:
[
...
TimeseriesTableWidgetComponent,
...
],
...
exports: [
...
TimeseriesTableWidgetComponent,
...
],
...
})
export class WidgetComponentsModule { }
```
<br>
<br>

135
ui-ngx/src/assets/help/en_US/widget/editor/widget_js_fn.md

@ -0,0 +1,135 @@
#### Widget type JavaScript code
<div class="divider"></div>
<br/>
All widget related JavaScript code according to the [Widget API{:target="_blank"}](${baseUrl}/docs/user-guide/contribution/widgets-development/#basic-widget-api).
The built-in variable **self** is a reference to the widget instance.<br>
Each widget function should be defined as a property of the **self** variable.
**self** variable has property **ctx** of type [WidgetContext{:target="_blank"}](https://github.com/thingsboard/thingsboard/blob/5bb6403407aa4898084832d6698aa9ea6d484889/ui-ngx/src/app/modules/home/models/widget-component.models.ts#L107) - a reference to widget context that has all necessary API and data used by widget instance.
In order to implement a new widget, the following JavaScript functions should be defined *(Note: each function is optional and can be implemented according to widget specific behaviour):*
|{:auto} **Function** | **Description** |
|------------------------------------|----------------------------------------------------------------------------------------|
| ``` onInit() ``` | The first function which is called when widget is ready for initialization. Should be used to prepare widget DOM, process widget settings and initial subscription information. |
| ``` onDataUpdated() ``` | Called when the new data is available from the widget subscription. Latest data can be accessed from the <span trigger-style="fontSize: 16px;" trigger-text="<b>defaultSubscription</b>" tb-help-popup="widget/editor/widget_js_subscription_object"></span> object of widget context (**ctx**). |
| ``` onResize() ``` | Called when widget container is resized. Latest width and height can be obtained from widget context (**ctx**). |
| ``` onEditModeChanged() ``` | Called when dashboard editing mode is changed. Latest mode is handled by isEdit property of **ctx**. |
| ``` onMobileModeChanged() ``` | Called when dashboard view width crosses mobile breakpoint. Latest state is handled by isMobile property of **ctx**. |
| ``` onDestroy() ``` | Called when widget element is destroyed. Should be used to cleanup all resources if necessary. |
| ``` getSettingsSchema() ``` | Optional function returning widget settings schema json as alternative to **Settings schema** of settings section. |
| ``` getDataKeySettingsSchema() ``` | Optional function returning particular data key settings schema json as alternative to **Data key settings schema** tab of settings section. |
| ``` typeParameters() ``` | Returns [WidgetTypeParameters{:target="_blank"}](https://github.com/thingsboard/thingsboard/blob/2627fe51d491055d4140f16617ed543f7f5bd8f6/ui-ngx/src/app/shared/models/widget.models.ts#L151) object describing widget datasource parameters. See <span trigger-style="fontSize: 16px;" trigger-text="<b>Type parameters object</b>" tb-help-popup="widget/editor/widget_js_type_parameters_object"></span> | |
| ``` actionSources() ``` | Returns map describing available widget action sources ([WidgetActionSource{:target="_blank"}](https://github.com/thingsboard/thingsboard/blob/2627fe51d491055d4140f16617ed543f7f5bd8f6/ui-ngx/src/app/shared/models/widget.models.ts#L121)) used to define user actions. See <span trigger-style="fontSize: 16px;" trigger-text="<b>Action sources object</b>" tb-help-popup="widget/editor/widget_js_action_sources_object"></span> |
<div class="divider"></div>
##### Creating simple widgets
The tutorials below show how to create minimal widgets of each type.
In order to minimize the amount of code, the Angular framework will be used, on which ThingsBoard UI is actually based.
By the way, you can always use pure JavaScript or jQuery API in your widget code.
<br>
<div style="padding-left: 64px;"
tb-help-popup="widget/editor/examples/latest_values_widget"
tb-help-popup-placement="top"
trigger-style="font-size: 16px;"
trigger-text="Sample Latest Values widget">
</div>
<br>
<div style="padding-left: 64px;"
tb-help-popup="widget/editor/examples/timeseries_widget"
tb-help-popup-placement="top"
trigger-style="font-size: 16px;"
trigger-text="Sample Time-Series widget">
</div>
<br>
<div style="padding-left: 64px;"
tb-help-popup="widget/editor/examples/rpc_widget"
tb-help-popup-placement="top"
trigger-style="font-size: 16px;"
trigger-text="Sample RPC (Control) widget">
</div>
<br>
<div style="padding-left: 64px;"
tb-help-popup="widget/editor/examples/alarm_widget"
tb-help-popup-placement="top"
trigger-style="font-size: 16px;"
trigger-text="Sample Alarm widget">
</div>
<br>
<div style="padding-left: 64px;"
tb-help-popup="widget/editor/examples/static_widget"
tb-help-popup-placement="top"
trigger-style="font-size: 16px;"
trigger-text="Sample Static widget">
</div>
<div class="divider"></div>
##### Integrating existing code to create widget definition
Below are some examples demonstrating how external JavaScript libraries or existing code can be reused/integrated to create new widgets.
###### Using external JavaScript library
<br>
<div style="padding-left: 64px;"
tb-help-popup="widget/editor/examples/ext_latest_values_example"
tb-help-popup-placement="top"
trigger-style="font-size: 16px;"
trigger-text="Latest Values widget Example with gauge.js library">
</div>
<br>
<div style="padding-left: 64px;"
tb-help-popup="widget/editor/examples/ext_timeseries_example"
tb-help-popup-placement="top"
trigger-style="font-size: 16px;"
trigger-text="Time-Series widget Example with Chart.js library">
</div>
###### Using existing JavaScript/Typescript code
<br>
<div style="padding-left: 64px;"
tb-help-popup="widget/editor/widget_js_existing_code"
tb-help-popup-placement="top"
trigger-style="font-size: 16px;"
trigger-text="Read more">
</div>
<div class="divider"></div>
##### Widget code debugging tips
The most simple method of debugging is Web console output.
Just place [**console.log(...)**{:target="_blank"}](https://developer.mozilla.org/en-US/docs/Web/API/Console/log) function inside any part of widget JavaScript code.
Then click **Run** button to restart widget code and observe debug information in the Web console.
Another and most effective method of debugging is to invoke browser debugger.
Put [**debugger;**{:target="_blank"}](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/debugger) statement into the place of widget code you are interested in and then click **Run** button to restart widget code.
Browser debugger (if enabled) will automatically pause code execution at the debugger statement and you will be able to analyze script execution using browser debugging tools.
<div class="divider"></div>
##### Further reading
For more information read [Widgets Development Guide{:target="_blank"}](${baseUrl}/docs/user-guide/contribution/widgets-development).
<br>
<br>

113
ui-ngx/src/assets/help/en_US/widget/editor/widget_js_subscription_object.md

@ -0,0 +1,113 @@
#### Subscription object
<div class="divider"></div>
<br/>
The widget subscription object is instance of [IWidgetSubscription{:target="_blank"}](https://github.com/thingsboard/thingsboard/blob/2627fe51d491055d4140f16617ed543f7f5bd8f6/ui-ngx/src/app/core/api/widget-api.models.ts#L264") and contains all subscription information, including current data, according to the [widget type{:target="_blank"}](${baseUrl}/docs/user-guide/ui/widget-library/#widget-types).
Depending on widget type, subscription object provides different data structures.
For [Latest values{:target="_blank"}](${baseUrl}/docs/user-guide/ui/widget-library/#latest-values) and [Time-series{:target="_blank"}](${baseUrl}/docs/user-guide/ui/widget-library/#time-series) widget types, it provides the following properties:
- **datasources** - array of datasources (Array<[Datasource{:target="_blank"}](https://github.com/thingsboard/thingsboard/blob/2627fe51d491055d4140f16617ed543f7f5bd8f6/ui-ngx/src/app/shared/models/widget.models.ts#L279)>) used by this subscription, using the following structure:
```javascript
datasources = [
{ // datasource
type: 'entity',// type of the datasource. Can be "function" or "entity"
name: 'name', // name of the datasource (in case of "entity" usually Entity name)
aliasName: 'aliasName', // name of the alias used to resolve this particular datasource Entity
entityName: 'entityName', // name of the Entity used as datasource
entityType: 'DEVICE', // datasource Entity type (for ex. "DEVICE", "ASSET", "TENANT", etc.)
entityId: '943b8cd0-576a-11e7-824c-0b1cb331ec92', // entity identificator presented as string uuid.
dataKeys: [ // array of keys (Array<DataKey>) (attributes or timeseries) of the entity used to fetch data
{ // dataKey
name: 'name', // the name of the particular entity attribute/timeseries
type: 'timeseries', // type of the dataKey. Can be "timeseries", "attribute" or "function"
label: 'Sin', // label of the dataKey. Used as display value (for ex. in the widget legend section)
color: '#ffffff', // color of the key. Can be used by widget to set color of the key data (for ex. lines in line chart or segments in the pie chart).
funcBody: "", // only applicable for datasource with type "function" and "function" key type. Defines body of the function to generate simulated data.
settings: {} // dataKey specific settings with structure according to the defined Data key settings json schema. See "Settings schema section".
},
//...
]
},
//...
]
```
- **data** - array of latest data (Array<[DatasourceData{:target="_blank"}](https://github.com/thingsboard/thingsboard/blob/2627fe51d491055d4140f16617ed543f7f5bd8f6/ui-ngx/src/app/shared/models/widget.models.ts#L310)>) received in scope of this subscription, using the following structure:
```javascript
data = [
{
datasource: {}, // datasource object of this data. See datasource structure above.
dataKey: {}, // dataKey for which the data is held. See dataKey structure above.
data: [ // array of data points
[ // data point
1498150092317, // unix timestamp of datapoint in milliseconds
1, // value, can be either string, numeric or boolean
],
//...
]
},
//...
]
```
For [Alarm widget{:target="_blank"}](${baseUrl}/docs/user-guide/ui/widget-library/#alarm-widget) type it provides the following properties:
- **alarmSource** - ([Datasource{:target="_blank"}](https://github.com/thingsboard/thingsboard/blob/2627fe51d491055d4140f16617ed543f7f5bd8f6/ui-ngx/src/app/shared/models/widget.models.ts#L279)) information about entity for which alarms are fetched, using the following structure:
```javascript
alarmSource = {
type: 'entity',// type of the alarm source. Can be "function" or "entity"
name: 'name', // name of the alarm source (in case of "entity" usually Entity name)
aliasName: 'aliasName', // name of the alias used to resolve this particular alarm source Entity
entityName: 'entityName', // name of the Entity used as alarm source
entityType: 'DEVICE', // alarm source Entity type (for ex. "DEVICE", "ASSET", "TENANT", etc.)
entityId: '943b8cd0-576a-11e7-824c-0b1cb331ec92', // entity identificator presented as string uuid.
dataKeys: [ // array of keys indicating alarm fields used to display alarms data
{ // dataKey
name: 'name', // the name of the particular alarm field
type: 'alarm', // type of the dataKey. Only "alarm" in this case.
label: 'Severity', // label of the dataKey. Used as display value (for ex. as a column title in the Alarms table)
color: '#ffffff', // color of the key. Can be used by widget to set color of the key data.
settings: {} // dataKey specific settings with structure according to the defined Data key settings json schema. See "Settings schema section".
},
//...
]
}
```
- **alarms** - array of alarms (Array<[Alarm{:target="_blank"}](https://github.com/thingsboard/thingsboard/blob/2627fe51d491055d4140f16617ed543f7f5bd8f6/ui-ngx/src/app/shared/models/alarm.models.ts#L89)>) received in scope of this subscription, using the following structure:
```javascript
alarms = [
{ // alarm
id: { // alarm id
entityType: "ALARM",
id: "943b8cd0-576a-11e7-824c-0b1cb331ec92"
},
createdTime: 1498150092317, // Alarm created time (unix timestamp)
startTs: 1498150092316, // Alarm started time (unix timestamp)
endTs: 1498563899065, // Alarm end time (unix timestamp)
ackTs: 0, // Time of alarm acknowledgment (unix timestamp)
clearTs: 0, // Time of alarm clear (unix timestamp)
originator: { // Originator - id of entity produced this alarm
entityType: "ASSET",
id: "ceb16a30-4142-11e7-8b30-d5d66714ea5a"
},
originatorName: "Originator Name", // Name of originator entity
type: "Temperature", // Type of the alarm
severity: "CRITICAL", // Severity of the alarm ("CRITICAL", "MAJOR", "MINOR", "WARNING", "INDETERMINATE")
status: "ACTIVE_UNACK", // Status of the alarm
// ("ACTIVE_UNACK" - active unacknowledged,
// "ACTIVE_ACK" - active acknowledged,
// "CLEARED_UNACK" - cleared unacknowledged,
// "CLEARED_ACK" - cleared acknowledged)
details: {} // Alarm details object derived from alarm details json.
}
]
```
For [RPC{:target="_blank"}](${baseUrl}/docs/user-guide/ui/widget-library/#rpc-control-widget) or [Static{:target="_blank"}](${baseUrl}/docs/user-guide/ui/widget-library/#static) widget types, subscription object is optional and does not contain necessary information.

14
ui-ngx/src/assets/help/en_US/widget/editor/widget_js_type_parameters_object.md

@ -0,0 +1,14 @@
#### Type parameters object
<div class="divider"></div>
<br/>
Object [WidgetTypeParameters{:target="_blank"}](https://github.com/thingsboard/thingsboard/blob/2627fe51d491055d4140f16617ed543f7f5bd8f6/ui-ngx/src/app/shared/models/widget.models.ts#L151) describing widget datasource parameters. It has the following properties:
```javascript
return {
maxDatasources: -1, // Maximum allowed datasources for this widget, -1 - unlimited
maxDataKeys: -1, //Maximum allowed data keys for this widget, -1 - unlimited
dataKeysOptional: false //Whether this widget can be configured with datasources without data keys
}
```

15
ui-ngx/src/styles.scss

@ -498,10 +498,12 @@ mat-label {
color: #0F161D;
line-height: normal;
font-weight: 500;
padding: 30px 32px 10px;
border-bottom: none;
margin: 0;
}
& > #{$heading} {
padding: 30px 32px 10px;
}
}
p {
@ -575,6 +577,9 @@ mat-label {
margin-bottom: 30px;
overflow: hidden;
table-layout: fixed;
&.auto {
table-layout: auto;
}
& > thead {
background-color: #f9fbff;
@ -587,10 +592,12 @@ mat-label {
padding: 12px 16px;
text-align: left;
margin: 0;
word-break: break-word;
@media screen and (max-width: 400px) {
font-size: 12px;
padding: 12px 4px;
code:not([class*=language-]) {
font-size: 12px;
}
}
}
}
@ -608,11 +615,13 @@ mat-label {
padding: 12px 16px;
text-align: left;
margin: 0;
word-break: break-word;
color: rgba(15, 22, 29, 0.8);
@media screen and (max-width: 400px) {
font-size: 12px;
padding: 12px 4px;
code:not([class*=language-]) {
font-size: 12px;
}
}
}
}

Loading…
Cancel
Save