UI: Fix markdown widgets blur (set use transform to false for gridster). Fix markdown blinking (cache angular/compiler module on dynamic component service). Fix echarts tooltip.

This commit is contained in:
Igor Kulikov 2025-03-20 19:16:37 +02:00
parent 8615dd4ce5
commit 73fc419537
9 changed files with 73 additions and 49 deletions

View File

@ -46,7 +46,7 @@
"canvas-gauges": "^2.1.7", "canvas-gauges": "^2.1.7",
"core-js": "^3.39.0", "core-js": "^3.39.0",
"dayjs": "1.11.13", "dayjs": "1.11.13",
"echarts": "https://github.com/thingsboard/echarts/archive/5.5.0-TB.tar.gz", "echarts": "https://github.com/thingsboard/echarts/archive/5.5.1-TB.tar.gz",
"flot": "https://github.com/thingsboard/flot.git#0.9-work", "flot": "https://github.com/thingsboard/flot.git#0.9-work",
"flot.curvedlines": "https://github.com/MichaelZinsmaier/CurvedLines.git#master", "flot.curvedlines": "https://github.com/MichaelZinsmaier/CurvedLines.git#master",
"font-awesome": "^4.7.0", "font-awesome": "^4.7.0",

View File

@ -15,9 +15,9 @@
/// ///
import { Component, Injectable, Type, ɵComponentDef, ɵNG_COMP_DEF } from '@angular/core'; import { Component, Injectable, Type, ɵComponentDef, ɵNG_COMP_DEF } from '@angular/core';
import { from, Observable, of } from 'rxjs'; import { from, Observable, shareReplay } from 'rxjs';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { mergeMap } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { guid } from '@core/utils'; import { guid } from '@core/utils';
@Injectable({ @Injectable({
@ -25,6 +25,10 @@ import { guid } from '@core/utils';
}) })
export class DynamicComponentFactoryService { export class DynamicComponentFactoryService {
private compiler$: Observable<any> = from(import('@angular/compiler')).pipe(
shareReplay({refCount: true, bufferSize: 1})
);
constructor() { constructor() {
} }
@ -34,14 +38,14 @@ export class DynamicComponentFactoryService {
imports?: Type<any>[], imports?: Type<any>[],
preserveWhitespaces?: boolean, preserveWhitespaces?: boolean,
styles?: string[]): Observable<Type<T>> { styles?: string[]): Observable<Type<T>> {
return from(import('@angular/compiler')).pipe( return this.compiler$.pipe(
mergeMap(() => { map(() => {
let componentImports: Type<any>[] = [CommonModule]; let componentImports: Type<any>[] = [CommonModule];
if (imports) { if (imports) {
componentImports = [...componentImports, ...imports]; componentImports = [...componentImports, ...imports];
} }
const comp = this.createAndCompileDynamicComponent(componentType, template, componentImports, preserveWhitespaces, styles); const comp = this.createAndCompileDynamicComponent(componentType, template, componentImports, preserveWhitespaces, styles);
return of(comp.type); return comp.type;
}) })
); );
} }

View File

@ -247,6 +247,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
defaultItemCols: 8, defaultItemCols: 8,
defaultItemRows: 6, defaultItemRows: 6,
displayGrid: this.displayGrid, displayGrid: this.displayGrid,
useTransformPositioning: false,
resizable: { resizable: {
enabled: this.isEdit && !this.isEditingWidget, enabled: this.isEdit && !this.isEditingWidget,
delayStart: 50, delayStart: 50,

View File

@ -270,8 +270,7 @@ export abstract class TbLatestChart<S extends LatestChartSettings> {
this.latestChartOption = { this.latestChartOption = {
tooltip: { tooltip: {
trigger: this.settings.showTooltip ? 'item' : 'none', trigger: this.settings.showTooltip ? 'item' : 'none',
confine: false, confine: true,
appendTo: 'body',
formatter: (params: CallbackDataParams) => formatter: (params: CallbackDataParams) =>
this.settings.showTooltip this.settings.showTooltip
? latestChartTooltipFormatter(this.renderer, this.settings, params, this.units, this.total, this.dataItems) ? latestChartTooltipFormatter(this.renderer, this.settings, params, this.units, this.total, this.dataItems)

View File

@ -161,6 +161,8 @@ export class TbTimeSeriesChart {
private latestData: FormattedData[] = []; private latestData: FormattedData[] = [];
private onParentScroll = this._onParentScroll.bind(this);
yMin$ = this.yMinSubject.asObservable(); yMin$ = this.yMinSubject.asObservable();
yMax$ = this.yMaxSubject.asObservable(); yMax$ = this.yMaxSubject.asObservable();
@ -358,6 +360,7 @@ export class TbTimeSeriesChart {
this.yMinSubject.complete(); this.yMinSubject.complete();
this.yMaxSubject.complete(); this.yMaxSubject.complete();
this.darkModeObserver?.disconnect(); this.darkModeObserver?.disconnect();
this.ctx.dashboard.gridster.el.removeEventListener('scroll', this.onParentScroll);
} }
public resize(): void { public resize(): void {
@ -611,6 +614,7 @@ export class TbTimeSeriesChart {
this.timeSeriesChart = echarts.init(this.chartElement, null, { this.timeSeriesChart = echarts.init(this.chartElement, null, {
renderer: 'svg' renderer: 'svg'
}); });
this.ctx.dashboard.gridster.el.addEventListener('scroll', this.onParentScroll);
this.timeSeriesChartOptions = { this.timeSeriesChartOptions = {
darkMode: this.darkMode, darkMode: this.darkMode,
backgroundColor: 'transparent', backgroundColor: 'transparent',
@ -837,6 +841,14 @@ export class TbTimeSeriesChart {
return this.settings.dataZoom ? 45 : 5; return this.settings.dataZoom ? 45 : 5;
} }
private _onParentScroll() {
if (this.timeSeriesChart) {
this.timeSeriesChart.dispatchAction({
type: 'hideTip'
});
}
}
private onResize() { private onResize() {
const shapeWidth = this.chartElement.offsetWidth; const shapeWidth = this.chartElement.offsetWidth;
const shapeHeight = this.chartElement.offsetHeight; const shapeHeight = this.chartElement.offsetHeight;

View File

@ -19,4 +19,4 @@
[additionalStyles]="additionalStyles" [additionalStyles]="additionalStyles"
[containerClass]="markdownClass" [containerClass]="markdownClass"
[applyDefaultMarkdownStyle]="applyDefaultMarkdownStyle" [applyDefaultMarkdownStyle]="applyDefaultMarkdownStyle"
[context]="{ ctx: ctx }" lineNumbers fallbackToPlainMarkdown (click)="markdownClick($event)"></tb-markdown> [context]="{ ctx: ctx, data: data }" lineNumbers fallbackToPlainMarkdown (click)="markdownClick($event)"></tb-markdown>

View File

@ -63,6 +63,8 @@ export class MarkdownWidgetComponent extends PageComponent implements OnInit {
@Input() @Input()
ctx: WidgetContext; ctx: WidgetContext;
data: FormattedData[];
markdownText: string; markdownText: string;
additionalStyles: string[]; additionalStyles: string[];
@ -128,15 +130,15 @@ export class MarkdownWidgetComponent extends PageComponent implements OnInit {
} else { } else {
initialData = []; initialData = [];
} }
const data = formattedDataFormDatasourceData(initialData); this.data = formattedDataFormDatasourceData(initialData);
let markdownText = this.settings.useMarkdownTextFunction ? const markdownText = this.settings.useMarkdownTextFunction ?
this.markdownTextFunction.pipe(map(markdownTextFunction => safeExecuteTbFunction(markdownTextFunction, [data, this.ctx]))) : this.settings.markdownTextPattern; this.markdownTextFunction.pipe(map(markdownTextFunction => safeExecuteTbFunction(markdownTextFunction, [this.data, this.ctx]))) : this.settings.markdownTextPattern;
if (typeof markdownText === 'string') { if (typeof markdownText === 'string') {
this.updateMarkdownText(markdownText, data); this.updateMarkdownText(markdownText, this.data);
} else { } else {
markdownText.subscribe((text) => { markdownText.subscribe((text) => {
this.updateMarkdownText(text, data); this.updateMarkdownText(text, this.data);
}); });
} }
} }
@ -146,8 +148,8 @@ export class MarkdownWidgetComponent extends PageComponent implements OnInit {
markdownText = createLabelFromPattern(markdownText, allData); markdownText = createLabelFromPattern(markdownText, allData);
if (this.markdownText !== markdownText) { if (this.markdownText !== markdownText) {
this.markdownText = this.utils.customTranslation(markdownText, markdownText); this.markdownText = this.utils.customTranslation(markdownText, markdownText);
this.cd.detectChanges();
} }
this.cd.markForCheck();
} }
markdownClick($event: MouseEvent) { markdownClick($event: MouseEvent) {

View File

@ -32,16 +32,14 @@ import {
ViewChild, ViewChild,
ViewContainerRef ViewContainerRef
} from '@angular/core'; } from '@angular/core';
import { HelpService } from '@core/services/help.service';
import { MarkdownService, PrismPlugin } from 'ngx-markdown'; import { MarkdownService, PrismPlugin } from 'ngx-markdown';
import { DynamicComponentFactoryService } from '@core/services/dynamic-component-factory.service'; import { DynamicComponentFactoryService } from '@core/services/dynamic-component-factory.service';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { SHARED_MODULE_TOKEN } from '@shared/components/tokens'; import { SHARED_MODULE_TOKEN } from '@shared/components/tokens';
import { deepClone, guid, isDefinedAndNotNull } from '@core/utils'; import { guid, isDefinedAndNotNull } from '@core/utils';
import { Observable, of, ReplaySubject } from 'rxjs'; import { Observable, of, ReplaySubject } from 'rxjs';
import { coerceBoolean } from '@shared/decorators/coercion'; import { coerceBoolean } from '@shared/decorators/coercion';
let defaultMarkdownStyle; let defaultMarkdownStyle: string;
@Component({ @Component({
selector: 'tb-markdown', selector: 'tb-markdown',
@ -70,12 +68,12 @@ export class TbMarkdownComponent implements OnChanges {
@Input() additionalStyles: string[]; @Input() additionalStyles: string[];
@Input() @Input()
get lineNumbers(): boolean { return this.lineNumbersValue; } @coerceBoolean()
set lineNumbers(value: boolean) { this.lineNumbersValue = coerceBooleanProperty(value); } lineNumbers = false;
@Input() @Input()
get fallbackToPlainMarkdown(): boolean { return this.fallbackToPlainMarkdownValue; } @coerceBoolean()
set fallbackToPlainMarkdown(value: boolean) { this.fallbackToPlainMarkdownValue = coerceBooleanProperty(value); } fallbackToPlainMarkdown = false;
@Input() @Input()
@coerceBoolean() @coerceBoolean()
@ -83,9 +81,6 @@ export class TbMarkdownComponent implements OnChanges {
@Output() ready = new EventEmitter<void>(); @Output() ready = new EventEmitter<void>();
private lineNumbersValue = false;
private fallbackToPlainMarkdownValue = false;
isMarkdownReady = false; isMarkdownReady = false;
error = null; error = null;
@ -93,8 +88,7 @@ export class TbMarkdownComponent implements OnChanges {
private tbMarkdownInstanceComponentRef: ComponentRef<any>; private tbMarkdownInstanceComponentRef: ComponentRef<any>;
private tbMarkdownInstanceComponentType: Type<any>; private tbMarkdownInstanceComponentType: Type<any>;
constructor(private help: HelpService, constructor(private cd: ChangeDetectorRef,
private cd: ChangeDetectorRef,
private zone: NgZone, private zone: NgZone,
public markdownService: MarkdownService, public markdownService: MarkdownService,
@Inject(SHARED_MODULE_TOKEN) private sharedModule: Type<any>, @Inject(SHARED_MODULE_TOKEN) private sharedModule: Type<any>,
@ -102,8 +96,19 @@ export class TbMarkdownComponent implements OnChanges {
private renderer: Renderer2) {} private renderer: Renderer2) {}
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {
if (isDefinedAndNotNull(this.data)) { for (const propName of Object.keys(changes)) {
this.zone.run(() => this.render(this.data)); const change = changes[propName];
if (propName === 'data' && change.currentValue !== change.previousValue) {
if (isDefinedAndNotNull(this.data)) {
this.zone.run(() => this.render(this.data));
}
} else if (propName === 'context' && !change.firstChange) {
if (this.context && this.tbMarkdownInstanceComponentRef) {
for (const propName of Object.keys(this.context)) {
this.tbMarkdownInstanceComponentRef.instance[propName] = this.context[propName];
}
}
}
} }
} }
@ -134,8 +139,8 @@ export class TbMarkdownComponent implements OnChanges {
if (this.applyDefaultMarkdownStyle) { if (this.applyDefaultMarkdownStyle) {
if (!defaultMarkdownStyle) { if (!defaultMarkdownStyle) {
const compDef = this.dynamicComponentFactoryService.getComponentDef(TbMarkdownComponent); const compDef = this.dynamicComponentFactoryService.getComponentDef(TbMarkdownComponent);
defaultMarkdownStyle = deepClone(compDef.styles[0]).replace(/\[_nghost\-%COMP%\]/g, '') defaultMarkdownStyle = compDef.styles[0].replace(/\[_nghost-%COMP%]/g, '')
.replace(/\[_ngcontent\-%COMP%\]/g, ''); .replace(/\[_ngcontent-%COMP%]/g, '');
} }
styles.push(defaultMarkdownStyle); styles.push(defaultMarkdownStyle);
} }
@ -149,7 +154,7 @@ export class TbMarkdownComponent implements OnChanges {
this.ready.emit(); this.ready.emit();
}); });
} else { } else {
const parent = this; const destroyMarkdownInstanceResources = this.destroyMarkdownInstanceResources.bind(this);
let compileModules = [this.sharedModule]; let compileModules = [this.sharedModule];
if (this.additionalCompileModules) { if (this.additionalCompileModules) {
compileModules = compileModules.concat(this.additionalCompileModules); compileModules = compileModules.concat(this.additionalCompileModules);
@ -157,13 +162,14 @@ export class TbMarkdownComponent implements OnChanges {
this.dynamicComponentFactoryService.createDynamicComponent( this.dynamicComponentFactoryService.createDynamicComponent(
class TbMarkdownInstance { class TbMarkdownInstance {
ngOnDestroy(): void { ngOnDestroy(): void {
parent.destroyMarkdownInstanceResources(); destroyMarkdownInstanceResources();
} }
}, },
template, template,
compileModules, compileModules,
true, styles true, styles
).subscribe((componentType) => { ).subscribe({
next: (componentType) => {
this.tbMarkdownInstanceComponentType = componentType; this.tbMarkdownInstanceComponentType = componentType;
const injector: Injector = Injector.create({providers: [], parent: this.markdownContainer.injector}); const injector: Injector = Injector.create({providers: [], parent: this.markdownContainer.injector});
try { try {
@ -187,20 +193,21 @@ export class TbMarkdownComponent implements OnChanges {
this.ready.emit(); this.ready.emit();
}); });
}, },
(error) => { error: (error) => {
readyObservable = this.handleError(template, error, styles); readyObservable = this.handleError(template, error, styles);
this.cd.detectChanges(); this.cd.detectChanges();
readyObservable.subscribe(() => { readyObservable.subscribe(() => {
this.ready.emit(); this.ready.emit();
}); });
}); }
});
} }
} }
private handleError(template: string, error, styles?: string[]): Observable<void> { private handleError(template: string, error: any, styles?: string[]): Observable<void> {
this.error = (error ? error + '' : 'Failed to render markdown!').replace(/\n/g, '<br>'); this.error = (error ? error + '' : 'Failed to render markdown!').replace(/\n/g, '<br>');
this.markdownContainer.clear(); this.markdownContainer.clear();
if (this.fallbackToPlainMarkdownValue) { if (this.fallbackToPlainMarkdown) {
return this.plainMarkdown(template, styles); return this.plainMarkdown(template, styles);
} else { } else {
return of(null); return of(null);
@ -209,7 +216,7 @@ export class TbMarkdownComponent implements OnChanges {
private plainMarkdown(template: string, styles?: string[]): Observable<void> { private plainMarkdown(template: string, styles?: string[]): Observable<void> {
const element = this.fallbackElement.nativeElement; const element = this.fallbackElement.nativeElement;
let styleElement; let styleElement: any;
if (styles?.length) { if (styles?.length) {
const markdownClass = 'tb-markdown-view-' + guid(); const markdownClass = 'tb-markdown-view-' + guid();
let innerStyle = styles.join('\n'); let innerStyle = styles.join('\n');
@ -244,7 +251,7 @@ export class TbMarkdownComponent implements OnChanges {
if (imgs.length) { if (imgs.length) {
let totalImages = imgs.length; let totalImages = imgs.length;
const imagesLoadedSubject = new ReplaySubject<void>(); const imagesLoadedSubject = new ReplaySubject<void>();
imgs.each((index, img) => { imgs.each((_index, img) => {
$(img).one('load error', () => { $(img).one('load error', () => {
totalImages--; totalImages--;
if (totalImages === 0) { if (totalImages === 0) {

View File

@ -4798,12 +4798,12 @@ eastasianwidth@^0.2.0:
resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb"
integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==
"echarts@https://github.com/thingsboard/echarts/archive/5.5.0-TB.tar.gz": "echarts@https://github.com/thingsboard/echarts/archive/5.5.1-TB.tar.gz":
version "5.5.0-TB" version "5.5.1-TB"
resolved "https://github.com/thingsboard/echarts/archive/5.5.0-TB.tar.gz#0b707b5cd2ae4699e9ced8b07ca49cb70189ae2a" resolved "https://github.com/thingsboard/echarts/archive/5.5.1-TB.tar.gz#8cf0cbb1b4c6161f0b587a1a649ff4f8eecbbf42"
dependencies: dependencies:
tslib "2.3.0" tslib "2.3.0"
zrender "5.5.0" zrender "https://github.com/thingsboard/zrender/archive/5.5.0-TB.tar.gz"
editorconfig@^1.0.4: editorconfig@^1.0.4:
version "1.0.4" version "1.0.4"
@ -10102,9 +10102,8 @@ zone.js@~0.14.10:
resolved "https://registry.yarnpkg.com/zone.js/-/zone.js-0.14.10.tgz#23b8b29687c6bffece996e5ee5b854050e7775c8" resolved "https://registry.yarnpkg.com/zone.js/-/zone.js-0.14.10.tgz#23b8b29687c6bffece996e5ee5b854050e7775c8"
integrity sha512-YGAhaO7J5ywOXW6InXNlLmfU194F8lVgu7bRntUF3TiG8Y3nBK0x1UJJuHUP/e8IyihkjCYqhCScpSwnlaSRkQ== integrity sha512-YGAhaO7J5ywOXW6InXNlLmfU194F8lVgu7bRntUF3TiG8Y3nBK0x1UJJuHUP/e8IyihkjCYqhCScpSwnlaSRkQ==
zrender@5.5.0: "zrender@https://github.com/thingsboard/zrender/archive/5.5.0-TB.tar.gz":
version "5.5.0" version "5.5.0-TB"
resolved "https://registry.yarnpkg.com/zrender/-/zrender-5.5.0.tgz#54d0d6c4eda81a96d9f60a9cd74dc48ea026bc1e" resolved "https://github.com/thingsboard/zrender/archive/5.5.0-TB.tar.gz#9605f08284436a9be86085e27f1c01b29a9923bf"
integrity sha512-O3MilSi/9mwoovx77m6ROZM7sXShR/O/JIanvzTwjN3FORfLSr81PsUGd7jlaYOeds9d8tw82oP44+3YucVo+w==
dependencies: dependencies:
tslib "2.3.0" tslib "2.3.0"