Browse Source

Merge branch 'develop/3.5.2' of https://github.com/thingsboard/thingsboard into customer-removing-alarm-unassign

pull/9035/head
imbeacon 3 years ago
parent
commit
8532dd033c
  1. 16
      application/src/main/data/json/system/widget_bundles/cards.json
  2. 5
      common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java
  3. 19
      common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java
  4. 2
      ui-ngx/src/app/core/api/widget-api.models.ts
  5. 10
      ui-ngx/src/app/core/api/widget-subscription.ts
  6. 15
      ui-ngx/src/app/core/services/dialog.service.ts
  7. 32
      ui-ngx/src/app/core/services/dynamic-component-factory.service.ts
  8. 20
      ui-ngx/src/app/core/utils.ts
  9. 2
      ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.html
  10. 32
      ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.ts
  11. 2
      ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html
  12. 3
      ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts
  13. 4
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html
  14. 3
      ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.scss
  15. 1
      ui-ngx/src/app/modules/home/components/widget/config/basic/alarm/alarms-table-basic-config.component.html
  16. 9
      ui-ngx/src/app/modules/home/components/widget/config/basic/alarm/alarms-table-basic-config.component.ts
  17. 1
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/entities-table-basic-config.component.html
  18. 9
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/entities-table-basic-config.component.ts
  19. 9
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/simple-card-basic-config.component.ts
  20. 1
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/timeseries-table-basic-config.component.html
  21. 9
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/timeseries-table-basic-config.component.ts
  22. 1
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-card-basic-config.component.html
  23. 9
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-card-basic-config.component.ts
  24. 1
      ui-ngx/src/app/modules/home/components/widget/config/basic/chart/flot-basic-config.component.html
  25. 9
      ui-ngx/src/app/modules/home/components/widget/config/basic/chart/flot-basic-config.component.ts
  26. 23
      ui-ngx/src/app/modules/home/components/widget/config/timewindow-config-panel.component.html
  27. 25
      ui-ngx/src/app/modules/home/components/widget/config/timewindow-config-panel.component.ts
  28. 97
      ui-ngx/src/app/modules/home/components/widget/config/timewindow-style-panel.component.html
  29. 54
      ui-ngx/src/app/modules/home/components/widget/config/timewindow-style-panel.component.scss
  30. 108
      ui-ngx/src/app/modules/home/components/widget/config/timewindow-style-panel.component.ts
  31. 25
      ui-ngx/src/app/modules/home/components/widget/config/timewindow-style.component.html
  32. 97
      ui-ngx/src/app/modules/home/components/widget/config/timewindow-style.component.ts
  33. 6
      ui-ngx/src/app/modules/home/components/widget/config/widget-config-components.module.ts
  34. 11
      ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog-container.component.ts
  35. 11
      ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog.service.ts
  36. 5
      ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts
  37. 30
      ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts
  38. 5
      ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.html
  39. 7
      ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.scss
  40. 17
      ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.ts
  41. 9
      ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.models.ts
  42. 38
      ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts
  43. 6
      ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.ts
  44. 5
      ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.ts
  45. 3
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/value-card-widget-settings.component.html
  46. 4
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/css-unit-select.component.ts
  47. 4
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-select.component.ts
  48. 6
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings-panel.component.html
  49. 5
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings-panel.component.ts
  50. 13
      ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts
  51. 1
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.html
  52. 6
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts
  53. 62
      ui-ngx/src/app/modules/home/components/widget/widget-container.component.html
  54. 28
      ui-ngx/src/app/modules/home/components/widget/widget-container.component.scss
  55. 16
      ui-ngx/src/app/modules/home/components/widget/widget-preview.component.ts
  56. 23
      ui-ngx/src/app/modules/home/components/widget/widget.component.ts
  57. 37
      ui-ngx/src/app/modules/home/models/dashboard-component.models.ts
  58. 76
      ui-ngx/src/app/modules/home/models/widget-component.models.ts
  59. 6
      ui-ngx/src/app/shared/components/color-input.component.ts
  60. 13
      ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.ts
  61. 1
      ui-ngx/src/app/shared/components/dialog/material-icons-dialog.component.html
  62. 16
      ui-ngx/src/app/shared/components/dialog/material-icons-dialog.component.ts
  63. 2
      ui-ngx/src/app/shared/components/icon.component.ts
  64. 12
      ui-ngx/src/app/shared/components/json-form/json-form.component.ts
  65. 18
      ui-ngx/src/app/shared/components/markdown.component.ts
  66. 24
      ui-ngx/src/app/shared/components/material-icon-select.component.ts
  67. 9
      ui-ngx/src/app/shared/components/material-icons.component.html
  68. 8
      ui-ngx/src/app/shared/components/material-icons.component.scss
  69. 9
      ui-ngx/src/app/shared/components/material-icons.component.ts
  70. 6
      ui-ngx/src/app/shared/components/phone-input.component.scss
  71. 32
      ui-ngx/src/app/shared/components/time/timewindow.component.html
  72. 17
      ui-ngx/src/app/shared/components/time/timewindow.component.scss
  73. 55
      ui-ngx/src/app/shared/components/time/timewindow.component.ts
  74. 28
      ui-ngx/src/app/shared/models/widget-settings.models.ts
  75. 5
      ui-ngx/src/app/shared/models/widget.models.ts
  76. 16
      ui-ngx/src/assets/locale/locale.constant-en_US.json

16
application/src/main/data/json/system/widget_bundles/cards.json

File diff suppressed because one or more lines are too long

5
common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java

@ -57,6 +57,8 @@ public class TbUtils {
List.class)));
parserConfig.addImport("decodeToJson", new MethodStub(TbUtils.class.getMethod("decodeToJson",
ExecutionContext.class, List.class)));
parserConfig.addImport("decodeToJson", new MethodStub(TbUtils.class.getMethod("decodeToJson",
ExecutionContext.class, String.class)));
parserConfig.addImport("stringToBytes", new MethodStub(TbUtils.class.getMethod("stringToBytes",
ExecutionContext.class, String.class)));
parserConfig.addImport("stringToBytes", new MethodStub(TbUtils.class.getMethod("stringToBytes",
@ -174,6 +176,9 @@ public class TbUtils {
public static Object decodeToJson(ExecutionContext ctx, List<Byte> bytesList) throws IOException {
return TbJson.parse(ctx, bytesToString(bytesList));
}
public static Object decodeToJson(ExecutionContext ctx, String jsonStr) throws IOException {
return TbJson.parse(ctx, jsonStr);
}
public static String bytesToString(List<Byte> bytesList) {
byte[] bytes = bytesFromList(bytesList);

19
common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java

@ -27,6 +27,7 @@ import org.mvel2.SandboxedParserConfiguration;
import org.mvel2.execution.ExecutionArrayList;
import org.mvel2.execution.ExecutionHashMap;
import java.io.IOException;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.util.ArrayList;
@ -348,6 +349,24 @@ public class TbUtilsTest {
Assert.assertEquals(0, Double.compare(doubleValRev, TbUtils.parseBytesToDouble(doubleVaList, 0, false)));
}
@Test
public void parseBytesDecodeToJson() throws IOException {
String expectedStr = "{\"hello\": \"world\"}";
ExecutionHashMap<String, Object> expectedJson = new ExecutionHashMap<>(1, ctx);
expectedJson.put("hello", "world");
List<Byte> expectedBytes = TbUtils.stringToBytes(ctx, expectedStr);
Object actualJson = TbUtils.decodeToJson(ctx, expectedBytes);
Assert.assertEquals(expectedJson,actualJson);
}
@Test
public void parseStringDecodeToJson() throws IOException {
String expectedStr = "{\"hello\": \"world\"}";
ExecutionHashMap<String, Object> expectedJson = new ExecutionHashMap<>(1, ctx);
expectedJson.put("hello", "world");
Object actualJson = TbUtils.decodeToJson(ctx, expectedStr);
Assert.assertEquals(expectedJson,actualJson);
}
private static List<Byte> toList(byte[] data) {
List<Byte> result = new ArrayList<>(data.length);
for (Byte b : data) {

2
ui-ngx/src/app/core/api/widget-api.models.ts

@ -284,6 +284,8 @@ export interface IWidgetSubscription {
legendData: LegendData;
readonly firstDatasource?: Datasource;
datasourcePages?: PageData<Datasource>[];
dataPages?: PageData<Array<DatasourceData>>[];
datasources?: Array<Datasource>;

10
ui-ngx/src/app/core/api/widget-subscription.ts

@ -160,6 +160,16 @@ export class WidgetSubscription implements IWidgetSubscription {
warnOnPageDataOverflow: boolean;
ignoreDataUpdateOnIntervalTick: boolean;
get firstDatasource(): Datasource {
if (this.type === widgetType.alarm) {
return this.alarmSource;
} else if (this.datasources?.length) {
return this.datasources[0];
} else {
return null;
}
}
datasourcePages: PageData<Datasource>[];
dataPages: PageData<Array<DatasourceData>>[];
entityDataListeners: Array<EntityDataListener>;

15
ui-ngx/src/app/core/services/dialog.service.ts

@ -21,11 +21,11 @@ import { TranslateService } from '@ngx-translate/core';
import { AuthService } from '@core/auth/auth.service';
import {
ColorPickerDialogComponent,
ColorPickerDialogData
ColorPickerDialogData, ColorPickerDialogResult
} from '@shared/components/dialog/color-picker-dialog.component';
import {
MaterialIconsDialogComponent,
MaterialIconsDialogData
MaterialIconsDialogData, MaterialIconsDialogResult
} from '@shared/components/dialog/material-icons-dialog.component';
import { ConfirmDialogComponent } from '@shared/components/dialog/confirm-dialog.component';
import { AlertDialogComponent } from '@shared/components/dialog/alert-dialog.component';
@ -96,8 +96,8 @@ export class DialogService {
return dialogRef.afterClosed();
}
colorPicker(color: string, colorClearButton = false): Observable<string> {
return this.dialog.open<ColorPickerDialogComponent, ColorPickerDialogData, string>(ColorPickerDialogComponent,
colorPicker(color: string, colorClearButton = false): Observable<ColorPickerDialogResult> {
return this.dialog.open<ColorPickerDialogComponent, ColorPickerDialogData, ColorPickerDialogResult>(ColorPickerDialogComponent,
{
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
@ -109,13 +109,14 @@ export class DialogService {
}).afterClosed();
}
materialIconPicker(icon: string): Observable<string> {
return this.dialog.open<MaterialIconsDialogComponent, MaterialIconsDialogData, string>(MaterialIconsDialogComponent,
materialIconPicker(icon: string, iconClearButton = false): Observable<MaterialIconsDialogResult> {
return this.dialog.open<MaterialIconsDialogComponent, MaterialIconsDialogData, MaterialIconsDialogResult>(MaterialIconsDialogComponent,
{
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
icon
icon,
iconClearButton
},
autoFocus: false
}).afterClosed();

32
ui-ngx/src/app/core/services/dynamic-component-factory.service.ts

@ -33,11 +33,17 @@ import { catchError, map, mergeMap } from 'rxjs/operators';
@NgModule()
export abstract class DynamicComponentModule implements OnDestroy {
// eslint-disable-next-line @angular-eslint/contextual-lifecycle
ngOnDestroy(): void {
}
}
interface DynamicComponentData<T> {
componentType: Type<T>;
componentModuleRef: NgModuleRef<DynamicComponentModule>;
}
interface DynamicComponentModuleData {
moduleRef: NgModuleRef<DynamicComponentModule>;
moduleType: Type<DynamicComponentModule>;
@ -48,22 +54,22 @@ interface DynamicComponentModuleData {
})
export class DynamicComponentFactoryService {
private dynamicComponentModulesMap = new Map<ComponentFactory<any>, DynamicComponentModuleData>();
private dynamicComponentModulesMap = new Map<Type<any>, DynamicComponentModuleData>();
constructor(private compiler: Compiler,
private injector: Injector) {
}
public createDynamicComponentFactory<T>(
public createDynamicComponent<T>(
componentType: Type<T>,
template: string,
modules?: Type<any>[],
preserveWhitespaces?: boolean,
compileAttempt = 1,
styles?: string[]): Observable<ComponentFactory<T>> {
styles?: string[]): Observable<DynamicComponentData<T>> {
return from(import('@angular/compiler')).pipe(
mergeMap(() => {
const comp = this.createDynamicComponent(componentType, template, preserveWhitespaces, styles);
const comp = this._createDynamicComponent(componentType, template, preserveWhitespaces, styles);
let moduleImports: Type<any>[] = [CommonModule];
if (modules) {
moduleImports = [...moduleImports, ...modules];
@ -82,17 +88,19 @@ export class DynamicComponentFactoryService {
this.compiler.clearCacheFor(module.moduleType);
throw e;
}
const factory = moduleRef.componentFactoryResolver.resolveComponentFactory(comp);
this.dynamicComponentModulesMap.set(factory, {
this.dynamicComponentModulesMap.set(comp, {
moduleRef,
moduleType: module.moduleType
});
return factory;
return {
componentType: comp,
componentModuleRef: moduleRef
};
}),
catchError((error) => {
if (compileAttempt === 1) {
ɵresetCompiledComponents();
return this.createDynamicComponentFactory(componentType, template, modules, preserveWhitespaces, ++compileAttempt, styles);
return this.createDynamicComponent(componentType, template, modules, preserveWhitespaces, ++compileAttempt, styles);
} else {
throw error;
}
@ -102,16 +110,16 @@ export class DynamicComponentFactoryService {
);
}
public destroyDynamicComponentFactory<T>(factory: ComponentFactory<T>) {
const moduleData = this.dynamicComponentModulesMap.get(factory);
public destroyDynamicComponent<T>(componentType: Type<T>) {
const moduleData = this.dynamicComponentModulesMap.get(componentType);
if (moduleData) {
moduleData.moduleRef.destroy();
this.compiler.clearCacheFor(moduleData.moduleType);
this.dynamicComponentModulesMap.delete(factory);
this.dynamicComponentModulesMap.delete(componentType);
}
}
private createDynamicComponent<T>(componentType: Type<T>, template: string, preserveWhitespaces?: boolean, styles?: string[]): Type<T> {
private _createDynamicComponent<T>(componentType: Type<T>, template: string, preserveWhitespaces?: boolean, styles?: string[]): Type<T> {
// noinspection AngularMissingOrInvalidDeclarationInModule
return Component({
template,

20
ui-ngx/src/app/core/utils.ts

@ -180,9 +180,7 @@ export function objToBase64(obj: any): string {
}
export function base64toString(b64Encoded: string): string {
return decodeURIComponent(atob(b64Encoded).split('').map((c) => {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
}).join(''));
return decodeURIComponent(atob(b64Encoded).split('').map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)).join(''));
}
export function objToBase64URI(obj: any): string {
@ -190,9 +188,7 @@ export function objToBase64URI(obj: any): string {
}
export function base64toObj(b64Encoded: string): any {
const json = decodeURIComponent(atob(b64Encoded).split('').map((c) => {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
}).join(''));
const json = decodeURIComponent(atob(b64Encoded).split('').map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)).join(''));
return JSON.parse(json);
}
@ -355,9 +351,7 @@ const SNAKE_CASE_REGEXP = /[A-Z]/g;
export function snakeCase(name: string, separator: string): string {
separator = separator || '_';
return name.replace(SNAKE_CASE_REGEXP, (letter, pos) => {
return (pos ? separator : '') + letter.toLowerCase();
});
return name.replace(SNAKE_CASE_REGEXP, (letter, pos) => (pos ? separator : '') + letter.toLowerCase());
}
export function getDescendantProp(obj: any, path: string): any {
@ -381,7 +375,7 @@ export function insertVariable(pattern: string, name: string, value: any): strin
return result;
}
export function createLabelFromDatasource(datasource: Datasource, pattern: string): string {
export const createLabelFromDatasource = (datasource: Datasource, pattern: string): string => {
let label = pattern;
if (!datasource) {
return label;
@ -406,7 +400,9 @@ export function createLabelFromDatasource(datasource: Datasource, pattern: strin
match = varsRegex.exec(pattern);
}
return label;
}
};
export const hasDatasourceLabelsVariables = (pattern: string): boolean => varsRegex.test(pattern) !== null;
export function formattedDataFormDatasourceData(input: DatasourceData[], dataIndex?: number): FormattedData[] {
return _(input).groupBy(el => el.datasource.entityName + el.datasource.entityType)
@ -694,7 +690,7 @@ export function getEntityDetailsPageURL(id: string, entityType: EntityType): str
}
export function parseHttpErrorMessage(errorResponse: HttpErrorResponse,
translate: TranslateService, responseType?: string): {message: string, timeout: number} {
translate: TranslateService, responseType?: string): {message: string; timeout: number} {
let error = null;
let errorMessage: string;
let timeout = 0;

2
ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.html

@ -17,7 +17,7 @@
-->
<form [formGroup]="attributeFormGroup" (ngSubmit)="add()" style="min-width: 400px;">
<mat-toolbar color="primary">
<h2>{{ 'attribute.add' | translate }}</h2>
<h2>{{ title | translate }}</h2>
<span fxFlex></span>
<button mat-icon-button
(click)="cancel()"

32
ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.ts

@ -19,16 +19,17 @@ import { ErrorStateMatcher } from '@angular/material/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms';
import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms';
import { EntityId } from '@shared/models/id/entity-id';
import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component';
import { AttributeData, AttributeScope } from '@shared/models/telemetry/telemetry.models';
import { AttributeData, AttributeScope, LatestTelemetry, TelemetryType } from '@shared/models/telemetry/telemetry.models';
import { AttributeService } from '@core/http/attribute.service';
import { Observable } from 'rxjs';
export interface AddAttributeDialogData {
entityId: EntityId;
attributeScope: AttributeScope;
attributeScope: TelemetryType;
}
@Component({
@ -40,17 +41,19 @@ export interface AddAttributeDialogData {
export class AddAttributeDialogComponent extends DialogComponent<AddAttributeDialogComponent, boolean>
implements OnInit, ErrorStateMatcher {
attributeFormGroup: UntypedFormGroup;
attributeFormGroup: FormGroup;
submitted = false;
title = '';
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: AddAttributeDialogData,
private attributeService: AttributeService,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<AddAttributeDialogComponent, boolean>,
public fb: UntypedFormBuilder) {
public fb: FormBuilder) {
super(store, router, dialogRef);
}
@ -59,9 +62,11 @@ export class AddAttributeDialogComponent extends DialogComponent<AddAttributeDia
key: ['', [Validators.required, Validators.maxLength(255)]],
value: [null, [Validators.required]]
});
this.title = this.data.attributeScope === LatestTelemetry.LATEST_TELEMETRY ?
'attribute.add-telemetry' : 'attribute.add'
}
isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean {
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
const customErrorState = !!(control && control.invalid && this.submitted);
return originalErrorState || customErrorState;
@ -78,11 +83,14 @@ export class AddAttributeDialogComponent extends DialogComponent<AddAttributeDia
key: this.attributeFormGroup.get('key').value.trim(),
value: this.attributeFormGroup.get('value').value
};
this.attributeService.saveEntityAttributes(this.data.entityId,
this.data.attributeScope, [attribute]).subscribe(
() => {
this.dialogRef.close(true);
}
);
let task: Observable<any>;
if (this.data.attributeScope === LatestTelemetry.LATEST_TELEMETRY) {
task = this.attributeService.saveEntityTimeseries(this.data.entityId,
this.data.attributeScope, [attribute]);
} else {
task = this.attributeService.saveEntityAttributes(this.data.entityId,
this.data.attributeScope as AttributeScope, [attribute]);
}
task.subscribe(() => this.dialogRef.close(true));
}
}

2
ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html

@ -34,7 +34,7 @@
</div>
<span fxFlex></span>
<button mat-icon-button
*ngIf="!isClientSideTelemetryTypeMap.get(attributeScope)"
*ngIf="attributeScope !== attributeScopeTypes.CLIENT_SCOPE"
[disabled]="isLoading$ | async"
(click)="addAttribute($event)"
matTooltip="{{ 'action.add' | translate }}"

3
ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts

@ -100,6 +100,7 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI
isClientSideTelemetryTypeMap = isClientSideTelemetryType;
latestTelemetryTypes = LatestTelemetry;
attributeScopeTypes = AttributeScope;
mode: 'default' | 'widget' = 'default';
@ -313,7 +314,7 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
entityId: this.entityIdValue,
attributeScope: this.attributeScope as AttributeScope
attributeScope: this.attributeScope
}
}).afterClosed().subscribe(
(res) => {

4
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html

@ -196,7 +196,6 @@
flatButton
[displayTimewindowValue]="false"
[isEdit]="true"
direction="left"
tooltipPosition="below"
aggregation="true"
timezone="true"
@ -205,7 +204,6 @@
<tb-timewindow fxHide.gt-lg
hideLabel
[isEdit]="true"
direction="left"
tooltipPosition="below"
aggregation="true"
timezone="true"
@ -217,7 +215,6 @@
<tb-timewindow fxHide.gt-md
[isEdit]="false"
hideLabel
direction="left"
tooltipPosition="below"
aggregation="true"
timezone="true"
@ -225,7 +222,6 @@
</tb-timewindow>
<tb-timewindow fxHide.lt-lg
[isEdit]="false"
direction="left"
tooltipPosition="below"
aggregation="true"
timezone="true"

3
ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.scss

@ -16,7 +16,8 @@
:host {
display: block;
.clear-alarm-rule {
max-width: 100%;
min-width: 0;
margin-right: 8px;
border: 2px groove rgba(0, 0, 0, .45);
border-radius: 4px;
padding: 8px;

1
ui-ngx/src/app/modules/home/components/widget/config/basic/alarm/alarms-table-basic-config.component.html

@ -75,6 +75,7 @@
</mat-slide-toggle>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-material-icon-select asBoxInput
iconClearButton
[color]="alarmsTableWidgetConfigForm.get('iconColor').value"
formControlName="titleIcon">
</tb-material-icon-select>

9
ui-ngx/src/app/modules/home/components/widget/config/basic/alarm/alarms-table-basic-config.component.ts

@ -23,7 +23,10 @@ import { WidgetConfigComponentData } from '@home/models/widget-component.models'
import { DataKey, Datasource, WidgetConfig } from '@shared/models/widget.models';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { isUndefined } from '@core/utils';
import { getTimewindowConfig } from '@home/components/widget/config/timewindow-config-panel.component';
import {
getTimewindowConfig,
setTimewindowConfig
} from '@home/components/widget/config/timewindow-config-panel.component';
@Component({
selector: 'tb-alarms-table-basic-config',
@ -74,9 +77,7 @@ export class AlarmsTableBasicConfigComponent extends BasicWidgetConfigComponent
}
protected prepareOutputConfig(config: any): WidgetConfigComponentData {
this.widgetConfig.config.useDashboardTimewindow = config.timewindowConfig.useDashboardTimewindow;
this.widgetConfig.config.displayTimewindow = config.timewindowConfig.displayTimewindow;
this.widgetConfig.config.timewindow = config.timewindowConfig.timewindow;
setTimewindowConfig(this.widgetConfig.config, config.timewindowConfig);
this.widgetConfig.config.alarmFilterConfig = config.alarmFilterConfig;
this.widgetConfig.config.alarmSource = config.datasources[0];
this.setColumns(config.columns, this.widgetConfig.config.alarmSource);

1
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/entities-table-basic-config.component.html

@ -64,6 +64,7 @@
</mat-slide-toggle>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-material-icon-select asBoxInput
iconClearButton
[color]="entitiesTableWidgetConfigForm.get('iconColor').value"
formControlName="titleIcon">
</tb-material-icon-select>

9
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/entities-table-basic-config.component.ts

@ -29,7 +29,10 @@ import {
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { isUndefined } from '@core/utils';
import { getTimewindowConfig } from '@home/components/widget/config/timewindow-config-panel.component';
import {
getTimewindowConfig,
setTimewindowConfig
} from '@home/components/widget/config/timewindow-config-panel.component';
@Component({
selector: 'tb-entities-table-basic-config',
@ -93,9 +96,7 @@ export class EntitiesTableBasicConfigComponent extends BasicWidgetConfigComponen
}
protected prepareOutputConfig(config: any): WidgetConfigComponentData {
this.widgetConfig.config.useDashboardTimewindow = config.timewindowConfig.useDashboardTimewindow;
this.widgetConfig.config.displayTimewindow = config.timewindowConfig.displayTimewindow;
this.widgetConfig.config.timewindow = config.timewindowConfig.timewindow;
setTimewindowConfig(this.widgetConfig.config, config.timewindowConfig);
this.widgetConfig.config.datasources = config.datasources;
this.setColumns(config.columns, this.widgetConfig.config.datasources);
this.widgetConfig.config.actions = config.actions;

9
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/simple-card-basic-config.component.ts

@ -27,7 +27,10 @@ import {
} from '@shared/models/widget.models';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { getTimewindowConfig } from '@home/components/widget/config/timewindow-config-panel.component';
import {
getTimewindowConfig,
setTimewindowConfig
} from '@home/components/widget/config/timewindow-config-panel.component';
import { isUndefined } from '@core/utils';
import { getLabel, setLabel } from '@shared/models/widget-settings.models';
@ -80,9 +83,7 @@ export class SimpleCardBasicConfigComponent extends BasicWidgetConfigComponent {
}
protected prepareOutputConfig(config: any): WidgetConfigComponentData {
this.widgetConfig.config.useDashboardTimewindow = config.timewindowConfig.useDashboardTimewindow;
this.widgetConfig.config.displayTimewindow = config.timewindowConfig.displayTimewindow;
this.widgetConfig.config.timewindow = config.timewindowConfig.timewindow;
setTimewindowConfig(this.widgetConfig.config, config.timewindowConfig);
this.widgetConfig.config.datasources = config.datasources;
setLabel(config.label, this.widgetConfig.config.datasources);
this.widgetConfig.config.actions = config.actions;

1
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/timeseries-table-basic-config.component.html

@ -64,6 +64,7 @@
</mat-slide-toggle>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-material-icon-select asBoxInput
iconClearButton
[color]="timeseriesTableWidgetConfigForm.get('iconColor').value"
formControlName="titleIcon">
</tb-material-icon-select>

9
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/timeseries-table-basic-config.component.ts

@ -24,7 +24,10 @@ import { DataKey, Datasource, WidgetConfig } from '@shared/models/widget.models'
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { deepClone, isUndefined } from '@core/utils';
import { getTimewindowConfig } from '@home/components/widget/config/timewindow-config-panel.component';
import {
getTimewindowConfig,
setTimewindowConfig
} from '@home/components/widget/config/timewindow-config-panel.component';
@Component({
selector: 'tb-timeseries-table-basic-config',
@ -79,9 +82,7 @@ export class TimeseriesTableBasicConfigComponent extends BasicWidgetConfigCompon
}
protected prepareOutputConfig(config: any): WidgetConfigComponentData {
this.widgetConfig.config.useDashboardTimewindow = config.timewindowConfig.useDashboardTimewindow;
this.widgetConfig.config.displayTimewindow = config.timewindowConfig.displayTimewindow;
this.widgetConfig.config.timewindow = config.timewindowConfig.timewindow;
setTimewindowConfig(this.widgetConfig.config, config.timewindowConfig);
this.widgetConfig.config.datasources = config.datasources;
this.setColumns(config.columns, this.widgetConfig.config.datasources);
this.widgetConfig.config.actions = config.actions;

1
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-card-basic-config.component.html

@ -65,6 +65,7 @@
</mat-form-field>
<tb-css-unit-select fxFlex formControlName="iconSizeUnit"></tb-css-unit-select>
<tb-material-icon-select asBoxInput
iconClearButton
[color]="valueCardWidgetConfigForm.get('iconColor').value?.color"
formControlName="icon">
</tb-material-icon-select>

9
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-card-basic-config.component.ts

@ -27,7 +27,10 @@ import {
} from '@shared/models/widget.models';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { getTimewindowConfig } from '@home/components/widget/config/timewindow-config-panel.component';
import {
getTimewindowConfig,
setTimewindowConfig
} from '@home/components/widget/config/timewindow-config-panel.component';
import { formatValue, isDefinedAndNotNull, isUndefined } from '@core/utils';
import {
DateFormatProcessor,
@ -145,9 +148,7 @@ export class ValueCardBasicConfigComponent extends BasicWidgetConfigComponent {
}
protected prepareOutputConfig(config: any): WidgetConfigComponentData {
this.widgetConfig.config.useDashboardTimewindow = config.timewindowConfig.useDashboardTimewindow;
this.widgetConfig.config.displayTimewindow = config.timewindowConfig.displayTimewindow;
this.widgetConfig.config.timewindow = config.timewindowConfig.timewindow;
setTimewindowConfig(this.widgetConfig.config, config.timewindowConfig);
this.widgetConfig.config.datasources = config.datasources;
this.widgetConfig.config.settings = this.widgetConfig.config.settings || {};

1
ui-ngx/src/app/modules/home/components/widget/config/basic/chart/flot-basic-config.component.html

@ -64,6 +64,7 @@
</mat-slide-toggle>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-material-icon-select asBoxInput
iconClearButton
[color]="flotWidgetConfigForm.get('iconColor').value"
formControlName="titleIcon">
</tb-material-icon-select>

9
ui-ngx/src/app/modules/home/components/widget/config/basic/chart/flot-basic-config.component.ts

@ -24,7 +24,10 @@ import { DataKey, Datasource, WidgetConfig } from '@shared/models/widget.models'
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { isUndefined } from '@core/utils';
import { getTimewindowConfig } from '@home/components/widget/config/timewindow-config-panel.component';
import {
getTimewindowConfig,
setTimewindowConfig
} from '@home/components/widget/config/timewindow-config-panel.component';
@Component({
selector: 'tb-flot-basic-config',
@ -83,9 +86,7 @@ export class FlotBasicConfigComponent extends BasicWidgetConfigComponent {
}
protected prepareOutputConfig(config: any): WidgetConfigComponentData {
this.widgetConfig.config.useDashboardTimewindow = config.timewindowConfig.useDashboardTimewindow;
this.widgetConfig.config.displayTimewindow = config.timewindowConfig.displayTimewindow;
this.widgetConfig.config.timewindow = config.timewindowConfig.timewindow;
setTimewindowConfig(this.widgetConfig.config, config.timewindowConfig);
this.widgetConfig.config.datasources = config.datasources;
this.setSeries(config.series, this.widgetConfig.config.datasources);
this.widgetConfig.config.actions = config.actions;

23
ui-ngx/src/app/modules/home/components/widget/config/timewindow-config-panel.component.html

@ -27,14 +27,19 @@
<mat-slide-toggle class="mat-slide" formControlName="displayTimewindow">
{{ 'widget-config.display-timewindow' | translate }}
</mat-slide-toggle>
<tb-timewindow asButton="true"
strokedButton
noMargin
isEdit="true"
alwaysDisplayTypePrefix
[historyOnly]="onlyHistoryTimewindow"
quickIntervalOnly="{{ widgetType === widgetTypes.latest }}"
aggregation="{{ widgetType === widgetTypes.timeseries }}"
formControlName="timewindow"></tb-timewindow>
<div fxFlex fxLayout="row" fxLayoutAlign="space-between center">
<tb-timewindow asButton="true"
strokedButton
noMargin
isEdit="true"
alwaysDisplayTypePrefix
[historyOnly]="onlyHistoryTimewindow"
quickIntervalOnly="{{ widgetType === widgetTypes.latest }}"
aggregation="{{ widgetType === widgetTypes.timeseries }}"
formControlName="timewindow"></tb-timewindow>
<tb-timewindow-style formControlName="timewindowStyle"
[previewValue]="timewindowConfig.get('timewindow').value">
</tb-timewindow-style>
</div>
</div>
</div>

25
ui-ngx/src/app/modules/home/components/widget/config/timewindow-config-panel.component.ts

@ -22,11 +22,13 @@ import { Timewindow } from '@shared/models/time/time.models';
import { TranslateService } from '@ngx-translate/core';
import { coerceBoolean } from '@shared/decorators/coercion';
import { isDefined } from '@core/utils';
import { TimewindowStyle } from '@shared/models/widget-settings.models';
export interface TimewindowConfigData {
useDashboardTimewindow: boolean;
displayTimewindow: boolean;
timewindow: Timewindow;
timewindowStyle: TimewindowStyle;
}
export const getTimewindowConfig = (config: WidgetConfig): TimewindowConfigData => ({
@ -34,9 +36,17 @@ export const getTimewindowConfig = (config: WidgetConfig): TimewindowConfigData
config.useDashboardTimewindow : true,
displayTimewindow: isDefined(config.displayTimewindow) ?
config.displayTimewindow : true,
timewindow: config.timewindow
timewindow: config.timewindow,
timewindowStyle: config.timewindowStyle
});
export const setTimewindowConfig = (config: WidgetConfig, data: TimewindowConfigData): void => {
config.useDashboardTimewindow = data.useDashboardTimewindow;
config.displayTimewindow = data.displayTimewindow;
config.timewindow = data.timewindow;
config.timewindowStyle = data.timewindowStyle;
};
@Component({
selector: 'tb-timewindow-config-panel',
templateUrl: './timewindow-config-panel.component.html',
@ -78,7 +88,8 @@ export class TimewindowConfigPanelComponent implements ControlValueAccessor, OnI
this.timewindowConfig = this.fb.group({
useDashboardTimewindow: [null, []],
displayTimewindow: [null, []],
timewindow: [null, []]
timewindow: [null, []],
timewindowStyle: [null, []]
});
this.timewindowConfig.valueChanges.subscribe(
(val) => this.propagateChange(val)
@ -86,6 +97,9 @@ export class TimewindowConfigPanelComponent implements ControlValueAccessor, OnI
this.timewindowConfig.get('useDashboardTimewindow').valueChanges.subscribe(() => {
this.updateTimewindowConfigEnabledState();
});
this.timewindowConfig.get('displayTimewindow').valueChanges.subscribe(() => {
this.updateTimewindowConfigEnabledState();
});
}
writeValue(data?: TimewindowConfigData): void {
@ -112,12 +126,19 @@ export class TimewindowConfigPanelComponent implements ControlValueAccessor, OnI
private updateTimewindowConfigEnabledState() {
const useDashboardTimewindow: boolean = this.timewindowConfig.get('useDashboardTimewindow').value;
const displayTimewindow: boolean = this.timewindowConfig.get('displayTimewindow').value;
if (useDashboardTimewindow) {
this.timewindowConfig.get('displayTimewindow').disable({emitEvent: false});
this.timewindowConfig.get('timewindow').disable({emitEvent: false});
this.timewindowConfig.get('timewindowStyle').disable({emitEvent: false});
} else {
this.timewindowConfig.get('displayTimewindow').enable({emitEvent: false});
this.timewindowConfig.get('timewindow').enable({emitEvent: false});
if (displayTimewindow) {
this.timewindowConfig.get('timewindowStyle').enable({emitEvent: false});
} else {
this.timewindowConfig.get('timewindowStyle').disable({emitEvent: false});
}
}
}

97
ui-ngx/src/app/modules/home/components/widget/config/timewindow-style-panel.component.html

@ -0,0 +1,97 @@
<!--
Copyright © 2016-2023 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.
-->
<div class="tb-timewindow-style-panel" [formGroup]="timewindowStyleFormGroup">
<div class="tb-timewindow-style-title" translate>timewindow.style</div>
<div class="tb-form-row space-between">
<mat-slide-toggle class="mat-slide" formControlName="showIcon">
{{ 'timewindow.icon' | translate }}
</mat-slide-toggle>
<div fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="iconSize">
</mat-form-field>
<tb-material-icon-select asBoxInput
iconClearButton
formControlName="icon">
</tb-material-icon-select>
</div>
</div>
<div class="tb-form-row">
<div class="fixed-title-width" translate>timewindow.icon-position</div>
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="iconPosition" placeholder="{{ 'widget-config.set' | translate }}">
<mat-option [value]="'left'">
{{ 'timewindow.icon-position-left' | translate }}
</mat-option>
<mat-option [value]="'right'">
{{ 'timewindow.icon-position-right' | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="tb-form-row space-between">
<div translate>timewindow.font</div>
<tb-font-settings
clearButton
[previewText]="timewindowComponent.displayValue()"
[initialPreviewStyle]="{fontSize: '14px',
color: 'rgba(0, 0, 0, 0.87)',
opacity: '0.85',
lineHeight: 'normal',
letterSpacing: 'normal'}"
formControlName="font">
</tb-font-settings>
</div>
<div class="tb-form-row space-between">
<div translate>timewindow.color</div>
<tb-color-input
formControlName="color"
colorClearButton
asBoxInput>
</tb-color-input>
</div>
<mat-divider></mat-divider>
<div class="tb-form-row no-border no-padding timewindow-preview">
<div class="fixed-title-width" translate>timewindow.preview</div>
<tb-timewindow #timewindowComponent
disablePanel
alwaysDisplayTypePrefix
timezone="true"
noMargin
noPadding
[timewindowStyle]="previewTimewindowStyle"
[ngModel]="previewValue"
[ngModelOptions]="{standalone: true}">
</tb-timewindow>
</div>
<div class="tb-timewindow-style-panel-buttons">
<button mat-button
color="primary"
type="button"
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button
color="primary"
type="button"
(click)="applyTimewindowStyle()"
[disabled]="timewindowStyleFormGroup.invalid || !timewindowStyleFormGroup.dirty">
{{ 'action.apply' | translate }}
</button>
</div>
</div>

54
ui-ngx/src/app/modules/home/components/widget/config/timewindow-style-panel.component.scss

@ -0,0 +1,54 @@
/**
* Copyright © 2016-2023 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.
*/
@import '../../../../../../scss/constants';
.tb-timewindow-style-panel {
width: 100%;
display: flex;
flex-direction: column;
gap: 16px;
@media #{$mat-gt-xs} {
min-width: 320px;
}
.tb-timewindow-style-title {
font-size: 16px;
font-weight: 500;
line-height: 24px;
letter-spacing: 0.25px;
color: rgba(0, 0, 0, 0.87);
}
.tb-form-row {
.fixed-title-width {
min-width: 120px;
}
&.timewindow-preview {
align-items: flex-start;
tb-timewindow {
font-size: 14px;
opacity: .85;
}
}
}
.tb-timewindow-style-panel-buttons {
height: 60px;
display: flex;
flex-direction: row;
gap: 16px;
justify-content: flex-end;
align-items: flex-end;
}
}

108
ui-ngx/src/app/modules/home/components/widget/config/timewindow-style-panel.component.ts

@ -0,0 +1,108 @@
///
/// Copyright © 2016-2023 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.
///
import { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { defaultTimewindowStyle, TimewindowStyle } from '@shared/models/widget-settings.models';
import { TbPopoverComponent } from '@shared/components/popover.component';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Timewindow } from '@shared/models/time/time.models';
import { deepClone } from '@core/utils';
@Component({
selector: 'tb-timewindow-style-panel',
templateUrl: './timewindow-style-panel.component.html',
providers: [],
styleUrls: ['./timewindow-style-panel.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class TimewindowStylePanelComponent extends PageComponent implements OnInit {
@Input()
timewindowStyle: TimewindowStyle;
@Input()
previewValue: Timewindow;
@Input()
popover: TbPopoverComponent<TimewindowStylePanelComponent>;
@Output()
timewindowStyleApplied = new EventEmitter<TimewindowStyle>();
timewindowStyleFormGroup: UntypedFormGroup;
previewTimewindowStyle: TimewindowStyle;
constructor(private fb: UntypedFormBuilder,
protected store: Store<AppState>) {
super(store);
}
ngOnInit(): void {
const computedTimewindowStyle = {...defaultTimewindowStyle, ...(this.timewindowStyle || {})};
this.timewindowStyleFormGroup = this.fb.group(
{
showIcon: [computedTimewindowStyle.showIcon, []],
iconSize: [computedTimewindowStyle.iconSize, []],
icon: [computedTimewindowStyle.icon, []],
iconPosition: [computedTimewindowStyle.iconPosition, []],
font: [computedTimewindowStyle.font, []],
color: [computedTimewindowStyle.color, []]
}
);
this.updatePreviewStyle(this.timewindowStyle);
this.updateTimewindowStyleEnabledState();
this.timewindowStyleFormGroup.valueChanges.subscribe((timewindowStyle: TimewindowStyle) => {
if (this.timewindowStyleFormGroup.valid) {
this.updatePreviewStyle(timewindowStyle);
setTimeout(() => {this.popover?.updatePosition();}, 0);
}
});
this.timewindowStyleFormGroup.get('showIcon').valueChanges.subscribe(() => {
this.updateTimewindowStyleEnabledState();
});
}
cancel() {
this.popover?.hide();
}
applyTimewindowStyle() {
const timewindowStyle = this.timewindowStyleFormGroup.getRawValue();
this.timewindowStyleApplied.emit(timewindowStyle);
}
private updateTimewindowStyleEnabledState() {
const showIcon: boolean = this.timewindowStyleFormGroup.get('showIcon').value;
if (showIcon) {
this.timewindowStyleFormGroup.get('iconSize').enable({emitEvent: false});
this.timewindowStyleFormGroup.get('icon').enable({emitEvent: false});
this.timewindowStyleFormGroup.get('iconPosition').enable({emitEvent: false});
} else {
this.timewindowStyleFormGroup.get('iconSize').disable({emitEvent: false});
this.timewindowStyleFormGroup.get('icon').disable({emitEvent: false});
this.timewindowStyleFormGroup.get('iconPosition').disable({emitEvent: false});
}
}
private updatePreviewStyle(timewindowStyle: TimewindowStyle) {
this.previewTimewindowStyle = deepClone(timewindowStyle);
}
}

25
ui-ngx/src/app/modules/home/components/widget/config/timewindow-style.component.html

@ -0,0 +1,25 @@
<!--
Copyright © 2016-2023 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.
-->
<button type="button"
mat-stroked-button
class="tb-box-button"
[disabled]="disabled"
#matButton
(click)="openTimewindowStylePopup($event, matButton)">
<tb-icon matButtonIcon>format_paint</tb-icon>
</button>

97
ui-ngx/src/app/modules/home/components/widget/config/timewindow-style.component.ts

@ -0,0 +1,97 @@
///
/// Copyright © 2016-2023 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.
///
import { Component, forwardRef, Input, OnInit, Renderer2, ViewContainerRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { TimewindowStyle } from '@shared/models/widget-settings.models';
import { MatButton } from '@angular/material/button';
import { TbPopoverService } from '@shared/components/popover.service';
import { Timewindow } from '@shared/models/time/time.models';
import { TimewindowStylePanelComponent } from '@home/components/widget/config/timewindow-style-panel.component';
@Component({
selector: 'tb-timewindow-style',
templateUrl: './timewindow-style.component.html',
styleUrls: [],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => TimewindowStyleComponent),
multi: true
}
]
})
export class TimewindowStyleComponent implements OnInit, ControlValueAccessor {
@Input()
disabled: boolean;
@Input()
previewValue: Timewindow;
private modelValue: TimewindowStyle;
private propagateChange = null;
constructor(private popoverService: TbPopoverService,
private renderer: Renderer2,
private viewContainerRef: ViewContainerRef) {}
ngOnInit(): void {
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}
writeValue(value: TimewindowStyle): void {
this.modelValue = value;
}
openTimewindowStylePopup($event: Event, matButton: MatButton) {
if ($event) {
$event.stopPropagation();
}
const trigger = matButton._elementRef.nativeElement;
if (this.popoverService.hasPopover(trigger)) {
this.popoverService.hidePopover(trigger);
} else {
const ctx: any = {
timewindowStyle: this.modelValue,
previewValue: this.previewValue
};
const timewindowStylePanelPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, TimewindowStylePanelComponent, 'left', true, null,
ctx,
{},
{}, {}, true);
timewindowStylePanelPopover.tbComponentRef.instance.popover = timewindowStylePanelPopover;
timewindowStylePanelPopover.tbComponentRef.instance.timewindowStyleApplied.subscribe((timewindowStyle) => {
timewindowStylePanelPopover.hide();
this.modelValue = timewindowStyle;
this.propagateChange(this.modelValue);
});
}
}
}

6
ui-ngx/src/app/modules/home/components/widget/config/widget-config-components.module.ts

@ -30,6 +30,8 @@ import { WidgetSettingsModule } from '@home/components/widget/lib/settings/widge
import { WidgetSettingsComponent } from '@home/components/widget/config/widget-settings.component';
import { TimewindowConfigPanelComponent } from '@home/components/widget/config/timewindow-config-panel.component';
import { WidgetSettingsCommonModule } from '@home/components/widget/lib/settings/common/widget-settings-common.module';
import { TimewindowStyleComponent } from '@home/components/widget/config/timewindow-style.component';
import { TimewindowStylePanelComponent } from '@home/components/widget/config/timewindow-style-panel.component';
@NgModule({
declarations:
@ -43,6 +45,8 @@ import { WidgetSettingsCommonModule } from '@home/components/widget/lib/settings
DatasourcesComponent,
EntityAliasSelectComponent,
FilterSelectComponent,
TimewindowStyleComponent,
TimewindowStylePanelComponent,
TimewindowConfigPanelComponent,
WidgetSettingsComponent
],
@ -62,6 +66,8 @@ import { WidgetSettingsCommonModule } from '@home/components/widget/lib/settings
DatasourcesComponent,
EntityAliasSelectComponent,
FilterSelectComponent,
TimewindowStyleComponent,
TimewindowStylePanelComponent,
TimewindowConfigPanelComponent,
WidgetSettingsComponent,
WidgetSettingsCommonModule

11
ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog-container.component.ts

@ -20,8 +20,8 @@ import {
ComponentFactory,
ComponentRef, HostBinding,
Inject,
Injector,
OnDestroy,
Injector, NgModuleRef,
OnDestroy, Type,
ViewContainerRef
} from '@angular/core';
import { DialogComponent } from '@shared/components/dialog.component';
@ -35,11 +35,13 @@ import {
} from '@home/components/widget/dialog/custom-dialog.component';
import { DialogService } from '@core/services/dialog.service';
import { TranslateService } from '@ngx-translate/core';
import { DynamicComponentModule } from '@core/services/dynamic-component-factory.service';
export interface CustomDialogContainerData {
controller: (instance: CustomDialogComponent) => void;
data?: any;
customComponentFactory: ComponentFactory<CustomDialogComponent>;
customComponentType: Type<CustomDialogComponent>;
customComponentModuleRef: NgModuleRef<DynamicComponentModule>;
}
@Component({
@ -77,7 +79,8 @@ export class CustomDialogContainerComponent extends DialogComponent<CustomDialog
}]
});
try {
this.customComponentRef = this.viewContainerRef.createComponent(this.data.customComponentFactory, 0, injector);
this.customComponentRef = this.viewContainerRef.createComponent(this.data.customComponentType,
{index: 0, injector, ngModuleRef: this.data.customComponentModuleRef});
} catch (e: any) {
let message;
if (e.message?.startsWith('NG0')) {

11
ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog.service.ts

@ -33,7 +33,7 @@ import { HOME_COMPONENTS_MODULE_TOKEN, SHARED_HOME_COMPONENTS_MODULE_TOKEN } fro
@Injectable()
export class CustomDialogService {
private customModules: Array<Type<any>>
private customModules: Array<Type<any>>;
constructor(
private translate: TranslateService,
@ -56,12 +56,13 @@ export class CustomDialogService {
if (Array.isArray(this.customModules)) {
modules.push(...this.customModules);
}
return this.dynamicComponentFactoryService.createDynamicComponentFactory(
return this.dynamicComponentFactoryService.createDynamicComponent(
class CustomDialogComponentInstance extends CustomDialogComponent {}, template, modules).pipe(
mergeMap((factory) => {
mergeMap((componentData) => {
const dialogData: CustomDialogContainerData = {
controller,
customComponentFactory: factory,
customComponentType: componentData.componentType,
customComponentModuleRef: componentData.componentModuleRef,
data
};
let dialogConfig: MatDialogConfig = {
@ -76,7 +77,7 @@ export class CustomDialogService {
CustomDialogContainerComponent,
dialogConfig).afterClosed().pipe(
tap(() => {
this.dynamicComponentFactoryService.destroyDynamicComponentFactory(factory);
this.dynamicComponentFactoryService.destroyDynamicComponent(componentData.componentType);
})
);
}

5
ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts

@ -15,7 +15,7 @@
///
import { PageComponent } from '@shared/components/page.component';
import { Directive, Injector, OnDestroy, OnInit } from '@angular/core';
import { Directive, Injector, OnDestroy, OnInit, TemplateRef } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { IDynamicWidgetComponent, WidgetContext } from '@home/models/widget-component.models';
@ -67,7 +67,8 @@ export class DynamicWidgetComponent extends PageComponent implements IDynamicWid
@TbInject(UntypedFormBuilder) public fb: UntypedFormBuilder,
@TbInject(Injector) public readonly $injector: Injector,
@TbInject('widgetContext') public readonly ctx: WidgetContext,
@TbInject('errorMessages') public readonly errorMessages: string[]) {
@TbInject('errorMessages') public readonly errorMessages: string[],
@TbInject('widgetTitlePanel') public readonly widgetTitlePanel: TemplateRef<any>) {
super(store);
this.ctx.$injector = $injector;
this.ctx.deviceService = $injector.get(DeviceService);

30
ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts

@ -37,16 +37,7 @@ import { DataKey, WidgetActionDescriptor, WidgetConfig } from '@shared/models/wi
import { IWidgetSubscription } from '@core/api/widget-api.models';
import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core';
import {
createLabelFromDatasource,
deepClone,
hashCode,
isDefined,
isDefinedAndNotNull,
isNumber,
isObject,
isUndefined
} from '@core/utils';
import { deepClone, hashCode, isDefined, isDefinedAndNotNull, isNumber, isObject, isUndefined } from '@core/utils';
import cssjs from '@core/css/css';
import { sortItems } from '@shared/models/page/page-link';
import { Direction } from '@shared/models/page/sort-order';
@ -194,8 +185,6 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
private subscription: IWidgetSubscription;
private widgetResize$: ResizeObserver;
private alarmsTitlePattern: string;
private displayActivity = false;
private displayDetails = true;
public allowAcknowledgment = true;
@ -322,7 +311,6 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
}
public onDataUpdated() {
this.updateTitle(true);
this.alarmsDatasource.updateAlarms();
this.clearCache();
this.ctx.detectChanges();
@ -342,13 +330,11 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
this.allowAssign = isDefined(this.settings.allowAssign) ? this.settings.allowAssign : true;
if (this.settings.alarmsTitle && this.settings.alarmsTitle.length) {
this.alarmsTitlePattern = this.utils.customTranslation(this.settings.alarmsTitle, this.settings.alarmsTitle);
this.ctx.widgetTitle = this.settings.alarmsTitle;
} else {
this.alarmsTitlePattern = this.translate.instant('alarm.alarms');
this.ctx.widgetTitle = this.translate.instant('alarm.alarms');
}
this.updateTitle(false);
this.enableSelection = isDefined(this.settings.enableSelection) ? this.settings.enableSelection : true;
if (!this.allowAcknowledgment && !this.allowClear) {
this.enableSelection = false;
@ -394,16 +380,6 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
$(this.elementRef.nativeElement).addClass(namespace);
}
private updateTitle(updateWidgetParams = false) {
const newTitle = createLabelFromDatasource(this.subscription.alarmSource, this.alarmsTitlePattern);
if (this.ctx.widgetTitle !== newTitle) {
this.ctx.widgetTitle = newTitle;
if (updateWidgetParams) {
this.ctx.updateWidgetParams();
}
}
}
private updateAlarmSource() {
if (this.enableSelection) {

5
ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.html

@ -17,6 +17,9 @@
-->
<div class="tb-value-card-panel" [class]="this.layout" [style]="backgroundStyle">
<div class="tb-value-card-overlay" [style]="overlayStyle"></div>
<div class="tb-value-card-title-panel">
<ng-container *ngTemplateOutlet="widgetTitlePanel"></ng-container>
</div>
<ng-container [ngSwitch]="layout">
<ng-template [ngSwitchCase]="valueCardLayout.square">
<ng-container *ngTemplateOutlet="iconWithLabelTpl"></ng-container>
@ -60,7 +63,7 @@
<tb-icon *ngIf="showIcon" [style]="iconStyle" [style.color]="iconColor.color">{{ icon }}</tb-icon>
</ng-template>
<ng-template #labelTpl>
<div *ngIf="showLabel" [style]="labelStyle" [style.color]="labelColor.color">{{ label }}</div>
<div *ngIf="showLabel" [style]="labelStyle" [style.color]="labelColor.color">{{ label$ | async }}</div>
</ng-template>
<ng-template #dateTpl>
<div *ngIf="showDate" [style]="dateStyle" [style.color]="dateColor.color">{{ dateFormat.formatted }}</div>

7
ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.scss

@ -44,6 +44,13 @@
bottom: 12px;
right: 12px;
}
> div.tb-value-card-title-panel {
position: absolute;
top: 12px;
left: 12px;
right: 12px;
z-index: 2;
}
.tb-value-card-icon-row {
display: flex;
flex-direction: row;

17
ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.ts

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { ChangeDetectorRef, Component, Input, OnInit } from '@angular/core';
import { ChangeDetectorRef, Component, Input, OnInit, TemplateRef } from '@angular/core';
import { WidgetContext } from '@home/models/widget-component.models';
import { formatValue, isDefinedAndNotNull } from '@core/utils';
import { DatePipe } from '@angular/common';
@ -31,6 +31,7 @@ import {
} from '@shared/models/widget-settings.models';
import { valueCardDefaultSettings, ValueCardLayout, ValueCardWidgetSettings } from './value-card-widget.models';
import { WidgetComponent } from '@home/components/widget/widget.component';
import { Observable } from 'rxjs';
@Component({
selector: 'tb-value-card-widget',
@ -46,6 +47,9 @@ export class ValueCardWidgetComponent implements OnInit {
@Input()
ctx: WidgetContext;
@Input()
widgetTitlePanel: TemplateRef<any>;
layout: ValueCardLayout;
showIcon = true;
icon = '';
@ -53,7 +57,7 @@ export class ValueCardWidgetComponent implements OnInit {
iconColor: ColorProcessor;
showLabel = true;
label = '';
label$: Observable<string>;
labelStyle: ComponentStyle = {};
labelColor: ColorProcessor;
@ -102,15 +106,16 @@ export class ValueCardWidgetComponent implements OnInit {
this.iconColor = ColorProcessor.fromSettings(this.settings.iconColor);
this.showLabel = this.settings.showLabel;
this.label = getLabel(this.ctx.datasources);
this.labelStyle = textStyle(this.settings.labelFont, '1.5', '0.25px');
const label = getLabel(this.ctx.datasources);
this.label$ = this.ctx.registerLabelPattern(label, this.label$);
this.labelStyle = textStyle(this.settings.labelFont, '0.25px');
this.labelColor = ColorProcessor.fromSettings(this.settings.labelColor);
this.valueStyle = textStyle(this.settings.valueFont, '100%', '0.13px');
this.valueStyle = textStyle(this.settings.valueFont, '0.13px');
this.valueColor = ColorProcessor.fromSettings(this.settings.valueColor);
this.showDate = this.settings.showDate;
this.dateFormat = DateFormatProcessor.fromSettings(this.ctx.$injector, this.settings.dateFormat);
this.dateStyle = textStyle(this.settings.dateFont, '1.33', '0.25px');
this.dateStyle = textStyle(this.settings.dateFont, '0.25px');
this.dateColor = ColorProcessor.fromSettings(this.settings.dateColor);
this.backgroundStyle = backgroundStyle(this.settings.background);

9
ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.models.ts

@ -89,7 +89,8 @@ export const valueCardDefaultSettings = (horizontal: boolean): ValueCardWidgetSe
size: 16,
sizeUnit: 'px',
style: 'normal',
weight: '500'
weight: '500',
lineHeight: '1.5'
},
labelColor: constantColor('rgba(0, 0, 0, 0.87)'),
showIcon: true,
@ -102,7 +103,8 @@ export const valueCardDefaultSettings = (horizontal: boolean): ValueCardWidgetSe
size: 52,
sizeUnit: 'px',
style: 'normal',
weight: '500'
weight: '500',
lineHeight: '100%'
},
valueColor: constantColor('rgba(0, 0, 0, 0.87)'),
showDate: true,
@ -112,7 +114,8 @@ export const valueCardDefaultSettings = (horizontal: boolean): ValueCardWidgetSe
size: 12,
sizeUnit: 'px',
style: 'normal',
weight: '500'
weight: '500',
lineHeight: '1.33'
},
dateColor: constantColor('rgba(0, 0, 0, 0.38)'),
background: {

38
ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts

@ -42,15 +42,7 @@ import {
import { IWidgetSubscription } from '@core/api/widget-api.models';
import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core';
import {
createLabelFromDatasource,
deepClone,
hashCode,
isDefined,
isNumber,
isObject,
isUndefined
} from '@core/utils';
import { deepClone, hashCode, isDefined, isNumber, isObject, isUndefined } from '@core/utils';
import cssjs from '@core/css/css';
import { CollectionViewer, DataSource } from '@angular/cdk/collections';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
@ -164,8 +156,6 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
private subscription: IWidgetSubscription;
private widgetResize$: ResizeObserver;
private entitiesTitlePattern: string;
private defaultPageSize = 10;
private defaultSortOrder = 'entityName';
@ -266,7 +256,6 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
}
public onDataUpdated() {
this.updateTitle(true);
this.entityDatasource.dataUpdated();
this.clearCache();
this.ctx.detectChanges();
@ -281,16 +270,15 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
this.setCellButtonAction = !!this.ctx.actionsApi.getActionDescriptors('actionCellButton').length;
this.hasRowAction = !!this.ctx.actionsApi.getActionDescriptors('rowClick').length || !!this.ctx.actionsApi.getActionDescriptors('rowDoubleClick').length;
this.hasRowAction = !!this.ctx.actionsApi.getActionDescriptors('rowClick').length ||
!!this.ctx.actionsApi.getActionDescriptors('rowDoubleClick').length;
if (this.settings.entitiesTitle && this.settings.entitiesTitle.length) {
this.entitiesTitlePattern = this.utils.customTranslation(this.settings.entitiesTitle, this.settings.entitiesTitle);
this.ctx.widgetTitle = this.settings.entitiesTitle;
} else {
this.entitiesTitlePattern = this.translate.instant('entity.entities');
this.ctx.widgetTitle = this.translate.instant('entity.entities');
}
this.updateTitle(false);
this.searchAction.show = isDefined(this.settings.enableSearch) ? this.settings.enableSearch : true;
this.displayPagination = isDefined(this.settings.displayPagination) ? this.settings.displayPagination : true;
this.enableStickyHeader = isDefined(this.settings.enableStickyHeader) ? this.settings.enableStickyHeader : true;
@ -319,16 +307,6 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
$(this.elementRef.nativeElement).addClass(namespace);
}
private updateTitle(updateWidgetParams = false) {
const newTitle = createLabelFromDatasource(this.subscription.datasources[0], this.entitiesTitlePattern);
if (this.ctx.widgetTitle !== newTitle) {
this.ctx.widgetTitle = newTitle;
if (updateWidgetParams) {
this.ctx.updateWidgetParams();
}
}
}
private updateDatasources() {
const displayEntityName = isDefined(this.settings.displayEntityName) ? this.settings.displayEntityName : true;
@ -498,14 +476,12 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
overlayRef.dispose();
});
const columns: DisplayColumn[] = this.columns.map(column => {
return {
const columns: DisplayColumn[] = this.columns.map(column => ({
title: column.title,
def: column.def,
display: this.displayedColumns.indexOf(column.def) > -1,
selectable: this.columnSelectionAvailability[column.def]
};
});
}));
const providers: StaticProvider[] = [
{

6
ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.ts

@ -94,12 +94,8 @@ export class JsonInputWidgetComponent extends PageComponent implements OnInit {
private initializeConfig() {
if (this.settings.widgetTitle && this.settings.widgetTitle.length) {
const title = createLabelFromDatasource(this.datasource, this.settings.widgetTitle);
this.ctx.widgetTitle = this.utils.customTranslation(title, title);
} else {
this.ctx.widgetTitle = this.ctx.widgetConfig.title;
this.ctx.widgetTitle = this.settings.widgetTitle;
}
if (this.settings.labelValue && this.settings.labelValue.length) {
const label = createLabelFromDatasource(this.datasource, this.settings.labelValue);
this.labelValue = this.utils.customTranslation(label, label);

5
ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.ts

@ -202,10 +202,7 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni
private initializeConfig() {
if (this.settings.widgetTitle && this.settings.widgetTitle.length) {
const titlePatternText = this.utils.customTranslation(this.settings.widgetTitle, this.settings.widgetTitle);
this.ctx.widgetTitle = createLabelFromDatasource(this.datasources[0], titlePatternText);
} else {
this.ctx.widgetTitle = this.ctx.widgetConfig.title;
this.ctx.widgetTitle = this.settings.widgetTitle;
}
this.settings.groupTitle = this.settings.groupTitle || '${entityName}';

3
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/value-card-widget-settings.component.html

@ -44,12 +44,13 @@
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showIcon">
{{ 'widgets.value-card.icon' | translate }}
</mat-slide-toggle>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field appearance="outline" class="flex number" subscriptSizing="dynamic">
<input matInput type="number" min="0" formControlName="iconSize" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-css-unit-select fxFlex formControlName="iconSizeUnit"></tb-css-unit-select>
<tb-material-icon-select asBoxInput
iconClearButton
[color]="valueCardWidgetSettingsForm.get('iconColor').value?.color"
formControlName="icon">
</tb-material-icon-select>

4
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/css-unit-select.component.ts

@ -67,9 +67,9 @@ export class CssUnitSelectComponent implements OnInit, ControlValueAccessor {
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.cssUnitFormControl.disable();
this.cssUnitFormControl.disable({emitEvent: false});
} else {
this.cssUnitFormControl.enable();
this.cssUnitFormControl.enable({emitEvent: false});
}
}

4
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-select.component.ts

@ -90,9 +90,9 @@ export class DateFormatSelectComponent implements OnInit, ControlValueAccessor {
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.dateFormatFormControl.disable();
this.dateFormatFormControl.disable({emitEvent: false});
} else {
this.dateFormatFormControl.enable();
this.dateFormatFormControl.enable({emitEvent: false});
}
}

6
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings-panel.component.html

@ -72,6 +72,12 @@
</mat-select>
</mat-form-field>
</div>
<div class="tb-form-row no-border no-padding">
<div class="fixed-title-width" translate>widgets.widget-font.line-height</div>
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="lineHeight" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<mat-divider></mat-divider>
<div class="tb-form-row no-border no-padding font-preview">
<div class="fixed-title-width" translate>widgets.widget-font.preview</div>

5
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings-panel.component.ts

@ -101,7 +101,8 @@ export class FontSettingsPanelComponent extends PageComponent implements OnInit
sizeUnit: [(this.font?.sizeUnit || 'px'), []],
family: [this.font?.family, []],
weight: [this.font?.weight, []],
style: [this.font?.style, []]
style: [this.font?.style, []],
lineHeight: [this.font?.lineHeight, []]
}
);
this.updatePreviewStyle(this.font);
@ -146,7 +147,7 @@ export class FontSettingsPanelComponent extends PageComponent implements OnInit
}
private updatePreviewStyle(font: Font) {
this.previewStyle = {...(this.initialPreviewStyle || {}), ...textStyle(font, '1')};
this.previewStyle = {...(this.initialPreviewStyle || {}), ...textStyle(font)};
}
}

13
ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts

@ -227,7 +227,7 @@ export class WidgetComponentService {
}
public clearWidgetInfo(widgetInfo: WidgetInfo, bundleAlias: string, widgetTypeAlias: string, isSystem: boolean): void {
this.dynamicComponentFactoryService.destroyDynamicComponentFactory(widgetInfo.componentFactory);
this.dynamicComponentFactoryService.destroyDynamicComponent(widgetInfo.componentType);
this.widgetService.deleteWidgetInfoFromCache(bundleAlias, widgetTypeAlias, isSystem);
}
@ -362,13 +362,14 @@ export class WidgetComponentService {
return of(resolvedModules);
} else {
this.registerWidgetSettingsForms(widgetInfo, resolvedModules.factories);
return this.dynamicComponentFactoryService.createDynamicComponentFactory(
return this.dynamicComponentFactoryService.createDynamicComponent(
class DynamicWidgetComponentInstance extends DynamicWidgetComponent {},
widgetInfo.templateHtml,
resolvedModules.modules
).pipe(
map((factory) => {
widgetInfo.componentFactory = factory;
map((componentData) => {
widgetInfo.componentType = componentData.componentType;
widgetInfo.componentModuleRef = componentData.componentModuleRef;
return null;
}),
catchError(e => {
@ -546,8 +547,8 @@ export class WidgetComponentService {
if (isUndefined(result.typeParameters.previewHeight)) {
result.typeParameters.previewHeight = '70%';
}
if (isUndefined(result.typeParameters.absoluteHeader)) {
result.typeParameters.absoluteHeader = false;
if (isUndefined(result.typeParameters.embedTitlePanel)) {
result.typeParameters.embedTitlePanel = false;
}
if (isFunction(widgetTypeInstance.actionSources)) {
result.actionSources = widgetTypeInstance.actionSources();

1
ui-ngx/src/app/modules/home/components/widget/widget-config.component.html

@ -70,6 +70,7 @@
<input matInput formControlName="iconSize">
</mat-form-field>
<tb-material-icon-select asBoxInput
iconClearButton
[color]="widgetSettings.get('iconColor').value"
formControlName="titleIcon">
</tb-material-icon-select>

6
ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts

@ -369,7 +369,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
this.dataSettings.addControl('timewindowConfig', this.fb.control({
useDashboardTimewindow: true,
displayTimewindow: true,
timewindow: null
timewindow: null,
timewindowStyle: null
}));
if (this.widgetType === widgetType.alarm) {
this.dataSettings.addControl('alarmFilterConfig', this.fb.control(null));
@ -517,7 +518,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
useDashboardTimewindow,
displayTimewindow: isDefined(config.displayTimewindow) ?
config.displayTimewindow : true,
timewindow: config.timewindow
timewindow: config.timewindow,
timewindowStyle: config.timewindowStyle
}, {emitEvent: false});
}
if (this.modelValue.isDataEnabled) {

62
ui-ngx/src/app/modules/home/components/widget/widget-container.component.html

@ -32,34 +32,13 @@
(click)="onClicked($event)"
(contextmenu)="onContextMenu($event)">
<div *ngIf="!!widgetComponent.widgetContext?.inited"
class="tb-widget-header"
[class]="{'tb-widget-header-absolute': widgetComponent.widgetContext?.absoluteHeader}">
<div *ngIf="widget.showWidgetTitlePanel"
class="tb-widget-title">
<span *ngIf="widget.showTitle"
[ngClass]="{'single-row': widget.hasTimewindow}"
[ngStyle]="widget.titleStyle"
[matTooltip]="widget.titleTooltip"
matTooltipClass="tb-tooltip-multiline"
matTooltipPosition="above"
class="mat-subtitle-1 title">
<tb-icon *ngIf="widget.showTitleIcon" [ngStyle]="widget.titleIconStyle">{{widget.titleIcon}}</tb-icon>
{{widget.customTranslatedTitle}}
</span>
<tb-timewindow *ngIf="widget.hasTimewindow"
aggregation="{{widget.hasAggregation}}"
quickIntervalOnly="{{widget.onlyQuickInterval}}"
historyOnly="{{widget.onlyHistoryTimewindow}}"
alwaysDisplayTypePrefix
timezone="true"
[isEdit]="isEdit"
[(ngModel)]="widgetComponent.widget.config.timewindow"
(ngModelChange)="widgetComponent.onTimewindowChanged($event)">
</tb-timewindow>
</div>
class="tb-widget-header">
<ng-container *ngIf="!widgetComponent.widgetContext?.embedTitlePanel">
<ng-container *ngTemplateOutlet="widgetTitlePanel"></ng-container>
</ng-container>
<div *ngIf="widget.showWidgetActions"
class="tb-widget-actions"
[class]="{'tb-widget-actions-absolute': !(widget.showWidgetTitlePanel&&(widget.showTitle||widget.hasAggregation))}"
[class]="{'tb-widget-actions-absolute': !(widget.showWidgetTitlePanel && !widgetComponent.widgetContext?.embedTitlePanel && (widget.showTitle||widget.hasAggregation))}"
(mousedown)="$event.stopPropagation()">
<button mat-icon-button *ngFor="let action of widget.customHeaderActions"
[fxShow]="!isEdit"
@ -109,7 +88,36 @@
<tb-widget #widgetComponent
[dashboardWidget]="widget"
[isEdit]="isEdit"
[isMobile]="isMobile">
[isMobile]="isMobile"
[widgetTitlePanel]="widgetTitlePanel">
</tb-widget>
</div>
</div>
<ng-template #widgetTitlePanel>
<div *ngIf="widget.showWidgetTitlePanel"
class="tb-widget-title">
<div *ngIf="widget.showTitle"
[matTooltip]="widget.titleTooltip"
matTooltipClass="tb-tooltip-multiline"
matTooltipPosition="above"
class="title-row">
<tb-icon *ngIf="widget.showTitleIcon" [style]="widget.titleIconStyle">{{widget.titleIcon}}</tb-icon>
<div class="mat-subtitle-1 title" [style]="widget.titleStyle">
{{widget.title$ | async}}
</div>
</div>
<tb-timewindow *ngIf="widget.hasTimewindow"
aggregation="{{widget.hasAggregation}}"
quickIntervalOnly="{{widget.onlyQuickInterval}}"
historyOnly="{{widget.onlyHistoryTimewindow}}"
alwaysDisplayTypePrefix
timezone="true"
noMargin
noPadding
[isEdit]="isEdit"
[timewindowStyle]="widgetComponent.widget.config.timewindowStyle"
[(ngModel)]="widgetComponent.widget.config.timewindow"
(ngModelChange)="widgetComponent.onTimewindowChanged($event)">
</tb-timewindow>
</div>
</ng-template>

28
ui-ngx/src/app/modules/home/components/widget/widget-container.component.scss

@ -37,13 +37,6 @@ div.tb-widget {
flex-direction: row;
place-content: flex-start space-between;
align-items: flex-start;
&-absolute {
position: absolute;
top: 0;
right: 0;
left: 0;
z-index: 1;
}
}
.tb-widget-title {
@ -59,23 +52,24 @@ div.tb-widget {
tb-timewindow {
font-size: 14px;
opacity: .85;
margin: 0;
}
.title {
.title-row {
display: flex;
flex-direction: row;
place-content: center flex-start;
align-items: center;
gap: 4px;
width: 100%;
}
.title {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
line-height: 24px;
letter-spacing: .01em;
letter-spacing: normal;
margin: 0;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
&.single-row{
-webkit-line-clamp: 1;
}
}
}

16
ui-ngx/src/app/modules/home/components/widget/widget-preview.component.ts

@ -77,13 +77,15 @@ export class WidgetPreviewComponent extends PageComponent implements OnInit, OnC
}
private loadPreviewWidget() {
const widget = deepClone(this.widget);
widget.sizeX = 24;
widget.sizeY = this.widget.sizeY * 2;
widget.row = 0;
widget.col = 0;
widget.config = this.widgetConfig;
this.widgets = [widget];
if (this.widget) {
const widget = deepClone(this.widget);
widget.sizeX = 24;
widget.sizeY = this.widget.sizeY * 2;
widget.row = 0;
widget.col = 0;
widget.config = this.widgetConfig;
this.widgets = [widget];
}
}
}

23
ui-ngx/src/app/modules/home/components/widget/widget.component.ts

@ -32,6 +32,7 @@ import {
Optional,
Renderer2,
SimpleChanges,
TemplateRef,
Type,
ViewChild,
ViewContainerRef,
@ -125,6 +126,9 @@ import { IModulesMap } from '@modules/common/modules-map.models';
})
export class WidgetComponent extends PageComponent implements OnInit, AfterViewInit, OnChanges, OnDestroy {
@Input()
widgetTitlePanel: TemplateRef<any>;
@Input()
isEdit: boolean;
@ -308,9 +312,6 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
this.loadFromWidgetInfo();
}
);
setTimeout(() => {
this.dashboardWidget.updateWidgetParams();
}, 0);
const noDataDisplayMessage = this.widget.config.noDataDisplayMessage;
if (isNotEmptyStr(noDataDisplayMessage)) {
@ -387,7 +388,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
this.handleWidgetException(e);
}
}
this.widgetContext.destroyed = true;
this.widgetContext.destroy();
this.destroyDynamicWidgetComponent();
}
}
@ -409,7 +410,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
elem.classList.add(this.widgetContext.widgetNamespace);
this.widgetType = this.widgetInfo.widgetTypeFunction;
this.typeParameters = this.widgetInfo.typeParameters;
this.widgetContext.absoluteHeader = this.typeParameters.absoluteHeader;
this.widgetContext.embedTitlePanel = this.typeParameters.embedTitlePanel;
if (!this.widgetType) {
this.widgetTypeInstance = {};
@ -480,6 +481,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
}
if (!this.widgetContext.inited && this.isReady()) {
this.widgetContext.inited = true;
this.dashboardWidget.updateWidgetParams();
this.widgetContext.detectContainerChanges();
if (this.cafs.init) {
this.cafs.init();
@ -493,7 +495,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
if (this.dataUpdatePending) {
this.widgetTypeInstance.onDataUpdated();
setTimeout(() => {
this.dashboardWidget.updateCustomHeaderActions(true);
this.dashboardWidget.updateParamsFromData(true);
}, 0);
this.dataUpdatePending = false;
}
@ -735,6 +737,10 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
{
provide: 'errorMessages',
useValue: this.errorMessages
},
{
provide: 'widgetTitlePanel',
useValue: this.widgetTitlePanel
}
],
parent: this.injector
@ -745,7 +751,8 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
this.widgetContext.$containerParent = $(containerElement);
try {
this.dynamicWidgetComponentRef = this.widgetContentContainer.createComponent(this.widgetInfo.componentFactory, 0, injector);
this.dynamicWidgetComponentRef = this.widgetContentContainer.createComponent(this.widgetInfo.componentType,
{index: 0, injector, ngModuleRef: this.widgetInfo.componentModuleRef});
this.cd.detectChanges();
} catch (e) {
if (this.dynamicWidgetComponentRef) {
@ -845,7 +852,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
if (this.widgetInstanceInited) {
this.widgetTypeInstance.onDataUpdated();
setTimeout(() => {
this.dashboardWidget.updateCustomHeaderActions(true);
this.dashboardWidget.updateParamsFromData(true);
}, 0);
} else {
this.dataUpdatePending = true;

37
ui-ngx/src/app/modules/home/models/dashboard-component.models.ts

@ -33,7 +33,7 @@ import { IAliasController, IStateController } from '@app/core/api/widget-api.mod
import { enumerable } from '@shared/decorators/enumerable';
import { UtilsService } from '@core/services/utils.service';
import { TbPopoverComponent } from '@shared/components/popover.component';
import { ComponentStyle, textStyle } from '@shared/models/widget-settings.models';
import { ComponentStyle, iconStyle, textStyle } from '@shared/models/widget-settings.models';
export interface WidgetsData {
widgets: Array<Widget>;
@ -331,8 +331,7 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget {
margin: string;
borderRadius: string;
title: string;
customTranslatedTitle: string;
title$: Observable<string>;
titleTooltip: string;
showTitle: boolean;
titleStyle: ComponentStyle;
@ -431,30 +430,23 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget {
this.margin = this.widget.config.margin || '0px';
this.borderRadius = this.widget.config.borderRadius;
this.title = isDefined(this.widgetContext.widgetTitle)
const title = isDefined(this.widgetContext.widgetTitle)
&& this.widgetContext.widgetTitle.length ? this.widgetContext.widgetTitle : this.widget.config.title;
this.customTranslatedTitle = this.dashboard.utils.customTranslation(this.title, this.title);
this.title$ = this.widgetContext.registerLabelPattern(title, this.title$);
this.titleTooltip = isDefined(this.widgetContext.widgetTitleTooltip)
&& this.widgetContext.widgetTitleTooltip.length ? this.widgetContext.widgetTitleTooltip : this.widget.config.titleTooltip;
this.titleTooltip = this.dashboard.utils.customTranslation(this.titleTooltip, this.titleTooltip);
this.showTitle = isDefined(this.widget.config.showTitle) ? this.widget.config.showTitle : true;
this.titleStyle = {...(this.widget.config.titleStyle || {}), ...textStyle(this.widget.config.titleFont, '24px', '0.01em')};
this.titleStyle = {...(this.widget.config.titleStyle || {}), ...textStyle(this.widget.config.titleFont, 'normal')};
if (this.widget.config.titleColor) {
this.titleStyle.color = this.widget.config.titleColor;
}
this.titleIcon = isDefined(this.widget.config.titleIcon) ? this.widget.config.titleIcon : '';
this.showTitleIcon = isDefined(this.widget.config.showTitleIcon) ? this.widget.config.showTitleIcon : false;
this.titleIconStyle = {};
this.titleIconStyle = this.widget.config.iconSize ? iconStyle(this.widget.config.iconSize) : {};
if (this.widget.config.iconColor) {
this.titleIconStyle.color = this.widget.config.iconColor;
}
if (this.widget.config.iconSize) {
this.titleIconStyle.width = this.widget.config.iconSize;
this.titleIconStyle.height = this.widget.config.iconSize;
this.titleIconStyle.fontSize = this.widget.config.iconSize;
this.titleIconStyle.lineHeight = this.widget.config.iconSize;
}
this.dropShadow = isDefined(this.widget.config.dropShadow) ? this.widget.config.dropShadow : true;
this.enableFullscreen = isDefined(this.widget.config.enableFullscreen) ? this.widget.config.enableFullscreen : true;
@ -497,14 +489,22 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget {
this.showWidgetActions = !this.widgetContext.hideTitlePanel;
this.updateCustomHeaderActions();
this.updateParamsFromData();
this.widgetActions = this.widgetContext.widgetActions ? this.widgetContext.widgetActions : [];
if (detectChanges) {
this.widgetContext.detectContainerChanges();
}
}
updateCustomHeaderActions(detectChanges = false) {
updateParamsFromData(detectChanges = false) {
this.widgetContext.updateLabelPatterns();
const update = this.updateCustomHeaderActions();
if (update && detectChanges) {
this.widgetContext.detectContainerChanges();
}
}
private updateCustomHeaderActions(): boolean {
let customHeaderActions: Array<WidgetHeaderAction>;
if (this.widgetContext.customHeaderActions) {
let data: FormattedData[] = [];
@ -517,10 +517,9 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget {
}
if (!isEqual(this.customHeaderActions, customHeaderActions)) {
this.customHeaderActions = customHeaderActions;
if (detectChanges) {
this.widgetContext.detectContainerChanges();
}
return true;
}
return false;
}
private filterCustomHeaderAction(action: WidgetHeaderAction, data: FormattedData[]): boolean {

76
ui-ngx/src/app/modules/home/models/widget-component.models.ts

@ -45,13 +45,13 @@ import {
WidgetActionsApi,
WidgetSubscriptionApi
} from '@core/api/widget-api.models';
import { ChangeDetectorRef, ComponentFactory, Injector, NgZone, Type } from '@angular/core';
import { ChangeDetectorRef, Injector, NgModuleRef, NgZone, Type } from '@angular/core';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { RafService } from '@core/services/raf.service';
import { WidgetTypeId } from '@shared/models/id/widget-type-id';
import { TenantId } from '@shared/models/id/tenant-id';
import { WidgetLayout } from '@shared/models/dashboard.models';
import { formatValue, isDefined } from '@core/utils';
import { createLabelFromDatasource, formatValue, hasDatasourceLabelsVariables, isDefined } from '@core/utils';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import {
@ -87,10 +87,12 @@ import * as RxJS from 'rxjs';
import * as RxJSOperators from 'rxjs/operators';
import { TbPopoverComponent } from '@shared/components/popover.component';
import { EntityId } from '@shared/models/id/entity-id';
import { AlarmQuery, AlarmSearchStatus, AlarmStatus} from '@app/shared/models/alarm.models';
import { AlarmQuery, AlarmSearchStatus, AlarmStatus } from '@app/shared/models/alarm.models';
import { MillisecondsToTimeStringPipe, TelemetrySubscriber } from '@app/shared/public-api';
import { UserId } from '@shared/models/id/user-id';
import { UserSettingsService } from '@core/http/user-settings.service';
import { DynamicComponentModule } from '@core/services/dynamic-component-factory.service';
import { BehaviorSubject, Observable } from 'rxjs';
export interface IWidgetAction {
name: string;
@ -200,6 +202,8 @@ export class WidgetContext {
subscriptions: {[id: string]: IWidgetSubscription} = {};
defaultSubscription: IWidgetSubscription = null;
labelPatterns = new Map<Observable<string>, LabelVariablePattern>();
timewindowFunctions: TimewindowFunctions = {
onUpdateTimewindow: (startTimeMs, endTimeMs, interval) => {
if (this.defaultSubscription) {
@ -265,7 +269,7 @@ export class WidgetContext {
hiddenData?: Array<{data: DataSet}>;
timeWindow?: WidgetTimewindow;
absoluteHeader?: boolean;
embedTitlePanel?: boolean;
hideTitlePanel = false;
@ -312,6 +316,23 @@ export class WidgetContext {
});
}
registerLabelPattern(label: string, label$: Observable<string>): Observable<string> {
let labelPattern = label$ ? this.labelPatterns.get(label$) : null;
if (labelPattern) {
labelPattern.setupPattern(label);
} else {
labelPattern = new LabelVariablePattern(label, this);
this.labelPatterns.set(labelPattern.label$, labelPattern);
}
return labelPattern.label$;
}
updateLabelPatterns() {
for (const labelPattern of this.labelPatterns.values()) {
labelPattern.update();
}
}
showSuccessToast(message: string, duration: number = 1000,
verticalPosition: NotificationVerticalPosition = 'bottom',
horizontalPosition: NotificationHorizontalPosition = 'left',
@ -406,6 +427,14 @@ export class WidgetContext {
this.widgetActions = undefined;
}
destroy() {
for (const labelPattern of this.labelPatterns.values()) {
labelPattern.destroy();
}
this.labelPatterns.clear();
this.destroyed = true;
}
closeDialog(resultData: any = null) {
const dialogRef = this.$scope.dialogRef || this.stateController.dashboardCtrl.dashboardCtx.getDashboard().dialogRef;
if (dialogRef) {
@ -426,6 +455,42 @@ export class WidgetContext {
}
}
export class LabelVariablePattern {
private pattern: string;
private hasVariables: boolean;
private labelSubject = new BehaviorSubject<string>('');
public label$ = this.labelSubject.asObservable();
constructor(label: string,
private ctx: WidgetContext) {
this.setupPattern(label);
}
setupPattern(label: string) {
this.pattern = this.ctx.dashboard.utils.customTranslation(label, label);
this.hasVariables = hasDatasourceLabelsVariables(this.pattern);
this.update();
}
update() {
let label = this.pattern;
const datasource = this.ctx.defaultSubscription?.firstDatasource;
if (this.hasVariables && datasource) {
label = createLabelFromDatasource(datasource, label);
}
if (this.labelSubject.value !== label) {
this.labelSubject.next(label);
}
}
destroy() {
this.labelSubject.complete();
}
}
export interface IDynamicWidgetComponent {
readonly ctx: WidgetContext;
readonly errorMessages: string[];
@ -446,7 +511,8 @@ export interface WidgetInfo extends WidgetTypeDescriptor, WidgetControllerDescri
typeLatestDataKeySettingsSchema?: string | any;
image?: string;
description?: string;
componentFactory?: ComponentFactory<IDynamicWidgetComponent>;
componentType?: Type<IDynamicWidgetComponent>;
componentModuleRef?: NgModuleRef<DynamicComponentModule>;
}
export interface WidgetConfigComponentData {

6
ui-ngx/src/app/shared/components/color-input.component.ts

@ -153,10 +153,10 @@ export class ColorInputComponent extends PageComponent implements OnInit, Contro
$event.stopPropagation();
this.dialogs.colorPicker(this.colorFormGroup.get('color').value,
this.colorClearButton).subscribe(
(color) => {
if (color) {
(result) => {
if (!result?.canceled) {
this.colorFormGroup.patchValue(
{color}, {emitEvent: true}
{color: result?.color}, {emitEvent: true}
);
this.cd.markForCheck();
}

13
ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.ts

@ -26,12 +26,17 @@ export interface ColorPickerDialogData {
colorClearButton: boolean;
}
export interface ColorPickerDialogResult {
color?: string;
canceled?: boolean;
}
@Component({
selector: 'tb-color-picker-dialog',
templateUrl: './color-picker-dialog.component.html',
styleUrls: ['./color-picker-dialog.component.scss']
})
export class ColorPickerDialogComponent extends DialogComponent<ColorPickerDialogComponent, string> {
export class ColorPickerDialogComponent extends DialogComponent<ColorPickerDialogComponent, ColorPickerDialogResult> {
color: string;
colorClearButton: boolean;
@ -39,18 +44,18 @@ export class ColorPickerDialogComponent extends DialogComponent<ColorPickerDialo
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: ColorPickerDialogData,
public dialogRef: MatDialogRef<ColorPickerDialogComponent, string>) {
public dialogRef: MatDialogRef<ColorPickerDialogComponent, ColorPickerDialogResult>) {
super(store, router, dialogRef);
this.color = data.color;
this.colorClearButton = data.colorClearButton;
}
selectColor(color: string) {
this.dialogRef.close(color);
this.dialogRef.close({color});
}
cancel(): void {
this.dialogRef.close(null);
this.dialogRef.close({canceled: true});
}
}

1
ui-ngx/src/app/shared/components/dialog/material-icons-dialog.component.html

@ -23,6 +23,7 @@
<mat-icon>close</mat-icon>
</button>
<tb-material-icons [selectedIcon]="selectedIcon"
[iconClearButton]="iconClearButton"
(iconSelected)="selectIcon($event)">
</tb-material-icons>
</div>

16
ui-ngx/src/app/shared/components/dialog/material-icons-dialog.component.ts

@ -23,6 +23,12 @@ import { DialogComponent } from '@shared/components/dialog.component';
export interface MaterialIconsDialogData {
icon: string;
iconClearButton: boolean;
}
export interface MaterialIconsDialogResult {
icon?: string;
canceled?: boolean;
}
@Component({
@ -31,24 +37,26 @@ export interface MaterialIconsDialogData {
providers: [],
styleUrls: ['./material-icons-dialog.component.scss']
})
export class MaterialIconsDialogComponent extends DialogComponent<MaterialIconsDialogComponent, string> {
export class MaterialIconsDialogComponent extends DialogComponent<MaterialIconsDialogComponent, MaterialIconsDialogResult> {
selectedIcon: string;
iconClearButton: boolean;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: MaterialIconsDialogData,
public dialogRef: MatDialogRef<MaterialIconsDialogComponent, string>) {
public dialogRef: MatDialogRef<MaterialIconsDialogComponent, MaterialIconsDialogResult>) {
super(store, router, dialogRef);
this.selectedIcon = data.icon;
this.iconClearButton = data.iconClearButton;
}
selectIcon(icon: string) {
this.dialogRef.close(icon);
this.dialogRef.close({icon});
}
cancel(): void {
this.dialogRef.close(null);
this.dialogRef.close({canceled: true});
}
}

2
ui-ngx/src/app/shared/components/icon.component.ts

@ -120,7 +120,7 @@ export class TbIconComponent extends _TbIconBase
this._contentChanges = this.contentObserver.observe(this._iconNameContent.nativeElement)
.subscribe(() => {
const content = this.viewValue;
if (content && this.icon !== content) {
if (this.icon !== content) {
this.icon = content;
this._updateIcon();
}

12
ui-ngx/src/app/shared/components/json-form/json-form.component.ts

@ -216,9 +216,9 @@ export class JsonFormComponent implements OnInit, ControlValueAccessor, Validato
private onColorClick(key: (string | number)[],
val: tinycolor.ColorFormats.RGBA,
colorSelectedFn: (color: tinycolor.ColorFormats.RGBA) => void) {
this.dialogs.colorPicker(tinycolor(val).toRgbString()).subscribe((color) => {
if (color && colorSelectedFn) {
colorSelectedFn(tinycolor(color).toRgb());
this.dialogs.colorPicker(tinycolor(val).toRgbString()).subscribe((result) => {
if (!result?.canceled && colorSelectedFn) {
colorSelectedFn(tinycolor(result?.color).toRgb());
}
});
}
@ -226,9 +226,9 @@ export class JsonFormComponent implements OnInit, ControlValueAccessor, Validato
private onIconClick(key: (string | number)[],
val: string,
iconSelectedFn: (icon: string) => void) {
this.dialogs.materialIconPicker(val).subscribe((icon) => {
if (icon && iconSelectedFn) {
iconSelectedFn(icon);
this.dialogs.materialIconPicker(val).subscribe((result) => {
if (!result?.canceled && iconSelectedFn) {
iconSelectedFn(result?.icon);
}
});
}

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

@ -17,7 +17,6 @@
import {
ChangeDetectorRef,
Component,
ComponentFactory,
ComponentRef,
ElementRef,
EventEmitter,
@ -91,7 +90,7 @@ export class TbMarkdownComponent implements OnChanges {
error = null;
private tbMarkdownInstanceComponentRef: ComponentRef<any>;
private tbMarkdownInstanceComponentFactory: ComponentFactory<any>;
private tbMarkdownInstanceComponentType: Type<any>;
constructor(private help: HelpService,
private cd: ChangeDetectorRef,
@ -153,7 +152,7 @@ export class TbMarkdownComponent implements OnChanges {
if (this.additionalCompileModules) {
compileModules = compileModules.concat(this.additionalCompileModules);
}
this.dynamicComponentFactoryService.createDynamicComponentFactory(
this.dynamicComponentFactoryService.createDynamicComponent(
class TbMarkdownInstance {
ngOnDestroy(): void {
parent.destroyMarkdownInstanceResources();
@ -162,12 +161,13 @@ export class TbMarkdownComponent implements OnChanges {
template,
compileModules,
true, 1, styles
).subscribe((factory) => {
this.tbMarkdownInstanceComponentFactory = factory;
).subscribe((componentData) => {
this.tbMarkdownInstanceComponentType = componentData.componentType;
const injector: Injector = Injector.create({providers: [], parent: this.markdownContainer.injector});
try {
this.tbMarkdownInstanceComponentRef =
this.markdownContainer.createComponent(this.tbMarkdownInstanceComponentFactory, 0, injector);
this.markdownContainer.createComponent(this.tbMarkdownInstanceComponentType,
{index: 0, injector, ngModuleRef: componentData.componentModuleRef});
if (this.context) {
for (const propName of Object.keys(this.context)) {
this.tbMarkdownInstanceComponentRef.instance[propName] = this.context[propName];
@ -261,9 +261,9 @@ export class TbMarkdownComponent implements OnChanges {
}
private destroyMarkdownInstanceResources() {
if (this.tbMarkdownInstanceComponentFactory) {
this.dynamicComponentFactoryService.destroyDynamicComponentFactory(this.tbMarkdownInstanceComponentFactory);
this.tbMarkdownInstanceComponentFactory = null;
if (this.tbMarkdownInstanceComponentType) {
this.dynamicComponentFactoryService.destroyDynamicComponent(this.tbMarkdownInstanceComponentType);
this.tbMarkdownInstanceComponentType = null;
}
this.tbMarkdownInstanceComponentRef = null;
}

24
ui-ngx/src/app/shared/components/material-icon-select.component.ts

@ -54,17 +54,9 @@ export class MaterialIconSelectComponent extends PageComponent implements OnInit
@Input()
disabled: boolean;
private iconClearButtonValue: boolean;
get iconClearButton(): boolean {
return this.iconClearButtonValue;
}
@Input()
set iconClearButton(value: boolean) {
const newVal = coerceBooleanProperty(value);
if (this.iconClearButtonValue !== newVal) {
this.iconClearButtonValue = newVal;
}
}
@coerceBoolean()
iconClearButton = false;
private requiredValue: boolean;
get required(): boolean {
@ -135,11 +127,12 @@ export class MaterialIconSelectComponent extends PageComponent implements OnInit
openIconDialog() {
if (!this.disabled) {
this.dialogs.materialIconPicker(this.materialIconFormGroup.get('icon').value).subscribe(
(icon) => {
if (icon) {
this.dialogs.materialIconPicker(this.materialIconFormGroup.get('icon').value,
this.iconClearButton).subscribe(
(result) => {
if (!result?.canceled) {
this.materialIconFormGroup.patchValue(
{icon}, {emitEvent: true}
{icon: result?.icon}, {emitEvent: true}
);
this.cd.markForCheck();
}
@ -159,7 +152,8 @@ export class MaterialIconSelectComponent extends PageComponent implements OnInit
const materialIconsPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, MaterialIconsComponent, 'left', true, null,
{
selectedIcon: this.materialIconFormGroup.get('icon').value
selectedIcon: this.materialIconFormGroup.get('icon').value,
iconClearButton: this.iconClearButton
},
{},
{}, {}, true);

9
ui-ngx/src/app/shared/components/material-icons.component.html

@ -62,4 +62,13 @@
<div class="tb-no-data-text">{{ 'icon.no-icons-found' | translate:{iconSearch: searchIconControl.value} }}</div>
</div>
</ng-container>
<div *ngIf="iconClearButton" class="tb-material-icons-panel-buttons">
<button mat-button
color="primary"
type="button"
(click)="clearIcon()"
[disabled]="!selectedIcon">
{{ 'action.clear' | translate }}
</button>
</div>
</div>

8
ui-ngx/src/app/shared/components/material-icons.component.scss

@ -58,4 +58,12 @@
margin: 0;
}
}
.tb-material-icons-panel-buttons {
height: 40px;
display: flex;
flex-direction: row;
gap: 16px;
align-items: flex-start;
align-self: flex-start;
}
}

9
ui-ngx/src/app/shared/components/material-icons.component.ts

@ -36,6 +36,7 @@ import { ResourcesService } from '@core/services/resources.service';
import { TbPopoverComponent } from '@shared/components/popover.component';
import { BreakpointObserver } from '@angular/cdk/layout';
import { MediaBreakpoints } from '@shared/models/constants';
import { coerceBoolean } from '@shared/decorators/coercion';
@Component({
selector: 'tb-material-icons',
@ -52,6 +53,10 @@ export class MaterialIconsComponent extends PageComponent implements OnInit {
@Input()
selectedIcon: string;
@Input()
@coerceBoolean()
iconClearButton = false;
@Input()
popover: TbPopoverComponent<MaterialIconsComponent>;
@ -123,6 +128,10 @@ export class MaterialIconsComponent extends PageComponent implements OnInit {
this.iconSelected.emit(icon.name);
}
clearIcon() {
this.iconSelected.emit(null);
}
private calculatePanelSize(iconsRowSize: number, iconRows = 4) {
this.iconsPanelHeight = Math.min(iconRows * this.iconsRowHeight, 10 * this.iconsRowHeight) + 'px';
this.iconsPanelWidth = (iconsRowSize * 36 + (iconsRowSize - 1) * 12 + 6) + 'px';

6
ui-ngx/src/app/shared/components/phone-input.component.scss

@ -24,7 +24,6 @@
.phone-input-container {
display: flex;
align-items: center;
.phone-input {
width: 100%;
@ -32,10 +31,11 @@
}
.flags-select-container {
display: inline-block;
display: flex;
align-items: center;
position: relative;
width: 50px;
height: 100%;
height: 56px;
margin-right: 5px;
}

32
ui-ngx/src/app/shared/components/time/timewindow.component.html

@ -41,26 +41,16 @@
</button>
<section *ngIf="!asButton"
class="tb-timewindow"
fxLayout="row"
fxLayoutAlign="start center">
<button *ngIf="direction === 'left'" [disabled]="timewindowDisabled" mat-icon-button class="tb-mat-32"
type="button"
(click)="toggleTimewindow($event)"
matTooltip="{{ 'timewindow.edit' | translate }}"
[matTooltipPosition]="tooltipPosition">
<mat-icon class="material-icons">query_builder</mat-icon>
</button>
<span [fxHide]="hideLabel"
(click)="toggleTimewindow($event)"
matTooltip="{{ 'timewindow.edit' | translate }}"
[matTooltipPosition]="tooltipPosition">
[class]="{'no-padding': noPadding}"
matTooltip="{{ 'timewindow.edit' | translate }}"
[matTooltipPosition]="tooltipPosition"
[style]="timewindowComponentStyle"
(click)="toggleTimewindow($event)">
<tb-icon *ngIf="computedTimewindowStyle.showIcon && computedTimewindowStyle.iconPosition === 'left'"
[style]="timewindowIconStyle">{{ computedTimewindowStyle.icon }}</tb-icon>
<div class="tb-timewindow-label" [fxHide]="hideLabel">
{{innerValue?.displayValue}} <span [fxShow]="innerValue?.displayTimezoneAbbr !== ''">| <span class="timezone-abbr">{{innerValue?.displayTimezoneAbbr}}</span></span>
</span>
<button *ngIf="direction === 'right'" [disabled]="timewindowDisabled" mat-icon-button class="tb-mat-32"
type="button"
(click)="toggleTimewindow($event)"
matTooltip="{{ 'timewindow.edit' | translate }}"
[matTooltipPosition]="tooltipPosition">
<mat-icon class="material-icons">query_builder</mat-icon>
</button>
</div>
<tb-icon *ngIf="computedTimewindowStyle.showIcon && computedTimewindowStyle.iconPosition === 'right'"
[style]="timewindowIconStyle">{{ computedTimewindowStyle.icon }}</tb-icon>
</section>

17
ui-ngx/src/app/shared/components/time/timewindow.component.scss

@ -24,15 +24,24 @@
max-width: 100%;
}
section.tb-timewindow {
min-height: 32px;
padding: 0 8px;
&.no-padding {
padding: 0;
}
line-height: 32px;
pointer-events: all;
cursor: pointer;
display: flex;
flex-direction: row;
place-content: center flex-start;
align-items: center;
gap: 4px;
width: 100%;
span {
.tb-timewindow-label {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
pointer-events: all;
cursor: pointer;
}
.timezone-abbr {

55
ui-ngx/src/app/shared/components/time/timewindow.component.ts

@ -20,7 +20,7 @@ import {
ElementRef,
forwardRef, HostBinding,
Injector,
Input,
Input, OnChanges, OnInit, SimpleChanges,
StaticProvider,
ViewContainerRef
} from '@angular/core';
@ -50,6 +50,12 @@ import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
import { ComponentPortal } from '@angular/cdk/portal';
import { coerceBoolean } from '@shared/decorators/coercion';
import {
ComponentStyle,
defaultTimewindowStyle, iconStyle,
textStyle,
TimewindowStyle
} from '@shared/models/widget-settings.models';
// @dynamic
@Component({
@ -64,7 +70,7 @@ import { coerceBoolean } from '@shared/decorators/coercion';
}
]
})
export class TimewindowComponent implements ControlValueAccessor {
export class TimewindowComponent implements ControlValueAccessor, OnInit, OnChanges {
historyOnlyValue = false;
@ -88,6 +94,14 @@ export class TimewindowComponent implements ControlValueAccessor {
@coerceBoolean()
noMargin = false;
@Input()
@coerceBoolean()
noPadding = false;
@Input()
@coerceBoolean()
disablePanel = false;
@Input()
@coerceBoolean()
forAllTimeEnabled = false;
@ -145,10 +159,10 @@ export class TimewindowComponent implements ControlValueAccessor {
}
@Input()
direction: 'left' | 'right' = 'left';
tooltipPosition: TooltipPosition = 'above';
@Input()
tooltipPosition: TooltipPosition = 'above';
timewindowStyle: TimewindowStyle;
@Input()
@coerceBoolean()
@ -158,6 +172,10 @@ export class TimewindowComponent implements ControlValueAccessor {
timewindowDisabled: boolean;
computedTimewindowStyle: TimewindowStyle;
timewindowComponentStyle: ComponentStyle;
timewindowIconStyle: ComponentStyle;
private propagateChange = (_: any) => {};
constructor(private overlay: Overlay,
@ -170,10 +188,28 @@ export class TimewindowComponent implements ControlValueAccessor {
public viewContainerRef: ViewContainerRef) {
}
ngOnInit() {
this.updateTimewindowStyle();
}
ngOnChanges(changes: SimpleChanges): void {
for (const propName of Object.keys(changes)) {
const change = changes[propName];
if (!change.firstChange && change.currentValue !== change.previousValue) {
if (propName === 'timewindowStyle') {
this.updateTimewindowStyle();
}
}
}
}
toggleTimewindow($event: Event) {
if ($event) {
$event.stopPropagation();
}
if (this.disablePanel) {
return;
}
const config = new OverlayConfig({
panelClass: 'tb-timewindow-panel',
backdropClass: 'cdk-overlay-transparent-backdrop',
@ -226,6 +262,17 @@ export class TimewindowComponent implements ControlValueAccessor {
this.cd.detectChanges();
}
private updateTimewindowStyle() {
if (!this.asButton) {
this.computedTimewindowStyle = {...defaultTimewindowStyle, ...(this.timewindowStyle || {})};
this.timewindowComponentStyle = textStyle(this.computedTimewindowStyle.font);
if (this.computedTimewindowStyle.color) {
this.timewindowComponentStyle.color = this.computedTimewindowStyle.color;
}
this.timewindowIconStyle = this.computedTimewindowStyle.iconSize ? iconStyle(this.computedTimewindowStyle.iconSize) : {};
}
}
private onHistoryOnlyChanged(): boolean {
if (this.historyOnlyValue && this.innerValue && this.innerValue.selectedTab !== TimewindowType.HISTORY) {
this.innerValue.selectedTab = TimewindowType.HISTORY;

28
ui-ngx/src/app/shared/models/widget-settings.models.ts

@ -60,6 +60,7 @@ export interface Font {
family: string;
weight: fontWeight;
style: fontStyle;
lineHeight: string;
}
export enum ColorType {
@ -89,6 +90,22 @@ export interface ColorSettings {
colorFunction?: string;
}
export interface TimewindowStyle {
showIcon: boolean;
icon: string;
iconSize: string;
iconPosition: 'left' | 'right';
font?: Font;
color?: string;
}
export const defaultTimewindowStyle: TimewindowStyle = {
showIcon: true,
icon: 'query_builder',
iconSize: '24px',
iconPosition: 'left'
};
export const constantColor = (color: string): ColorSettings => ({
type: ColorType.constant,
color,
@ -298,19 +315,19 @@ export interface BackgroundSettings {
overlay: OverlaySettings;
}
export const iconStyle = (size: number, sizeUnit: cssUnit): ComponentStyle => {
const iconSize = size + sizeUnit;
export const iconStyle = (size: number | string, sizeUnit: cssUnit = 'px'): ComponentStyle => {
const iconSize = typeof size === 'number' ? size + sizeUnit : size;
return {
width: iconSize,
minWidth: iconSize,
height: iconSize,
fontSize: iconSize,
lineHeight: iconSize
};
};
export const textStyle = (font?: Font, lineHeight = '1.5', letterSpacing = '0.25px'): ComponentStyle => {
export const textStyle = (font?: Font, letterSpacing = 'normal'): ComponentStyle => {
const style: ComponentStyle = {
lineHeight,
letterSpacing
};
if (font?.style) {
@ -319,6 +336,9 @@ export const textStyle = (font?: Font, lineHeight = '1.5', letterSpacing = '0.25
if (font?.weight) {
style.fontWeight = font.weight;
}
if (font?.lineHeight) {
style.lineHeight = font.lineHeight;
}
if (font?.size) {
style.fontSize = (font.size + (font.sizeUnit || 'px'));
}

5
ui-ngx/src/app/shared/models/widget.models.ts

@ -40,7 +40,7 @@ import { Dashboard } from '@shared/models/dashboard.models';
import { IAliasController } from '@core/api/widget-api.models';
import { isEmptyStr } from '@core/utils';
import { WidgetConfigComponentData } from '@home/models/widget-component.models';
import { ComponentStyle, Font } from '@shared/models/widget-settings.models';
import { ComponentStyle, Font, TimewindowStyle } from '@shared/models/widget-settings.models';
export enum widgetType {
timeseries = 'timeseries',
@ -183,7 +183,7 @@ export interface WidgetTypeParameters {
processNoDataByWidget?: boolean;
previewWidth?: string;
previewHeight?: string;
absoluteHeader?: boolean;
embedTitlePanel?: boolean;
}
export interface WidgetControllerDescriptor {
@ -633,6 +633,7 @@ export interface WidgetConfig {
useDashboardTimewindow?: boolean;
displayTimewindow?: boolean;
timewindow?: Timewindow;
timewindowStyle?: TimewindowStyle;
desktopHide?: boolean;
mobileHide?: boolean;
mobileHeight?: number;

16
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -725,7 +725,8 @@
"no-attributes-text": "No attributes found",
"no-telemetry-text": "No telemetry found",
"copy-key": "Copy key",
"copy-value": "Copy value"
"copy-value": "Copy value",
"add-telemetry": "Add telemetry"
},
"api-usage": {
"api-features": "API features",
@ -3902,7 +3903,15 @@
"interval": "Interval",
"just-now": "Just now",
"just-now-lower": "just now",
"ago": "ago"
"ago": "ago",
"style": "Timewindow style",
"icon": "Icon",
"icon-position": "Icon position",
"icon-position-left": "Left",
"icon-position-right": "Right",
"font": "Font",
"color": "Color",
"preview": "Preview"
},
"unit": {
"millimeter": "Millimeter",
@ -5799,7 +5808,8 @@
"font-weight-lighter": "Lighter",
"color": "Color",
"shadow-color": "Shadow color",
"preview": "Preview"
"preview": "Preview",
"line-height": "Line height"
},
"home": {
"no-data-available": "No data available"

Loading…
Cancel
Save