Browse Source

UI: Introduce timewindow style. Ability to embed widget title panel into widget template. Add pattern support for widget title, etc.

pull/9060/head
Igor Kulikov 3 years ago
parent
commit
a1fb657b0c
  1. 16
      application/src/main/data/json/system/widget_bundles/cards.json
  2. 15
      ui-ngx/src/app/core/services/dialog.service.ts
  3. 32
      ui-ngx/src/app/core/services/dynamic-component-factory.service.ts
  4. 20
      ui-ngx/src/app/core/utils.ts
  5. 4
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html
  6. 1
      ui-ngx/src/app/modules/home/components/widget/config/basic/alarm/alarms-table-basic-config.component.html
  7. 9
      ui-ngx/src/app/modules/home/components/widget/config/basic/alarm/alarms-table-basic-config.component.ts
  8. 1
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/entities-table-basic-config.component.html
  9. 9
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/entities-table-basic-config.component.ts
  10. 9
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/simple-card-basic-config.component.ts
  11. 1
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/timeseries-table-basic-config.component.html
  12. 9
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/timeseries-table-basic-config.component.ts
  13. 1
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-card-basic-config.component.html
  14. 9
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-card-basic-config.component.ts
  15. 1
      ui-ngx/src/app/modules/home/components/widget/config/basic/chart/flot-basic-config.component.html
  16. 9
      ui-ngx/src/app/modules/home/components/widget/config/basic/chart/flot-basic-config.component.ts
  17. 23
      ui-ngx/src/app/modules/home/components/widget/config/timewindow-config-panel.component.html
  18. 25
      ui-ngx/src/app/modules/home/components/widget/config/timewindow-config-panel.component.ts
  19. 97
      ui-ngx/src/app/modules/home/components/widget/config/timewindow-style-panel.component.html
  20. 54
      ui-ngx/src/app/modules/home/components/widget/config/timewindow-style-panel.component.scss
  21. 108
      ui-ngx/src/app/modules/home/components/widget/config/timewindow-style-panel.component.ts
  22. 25
      ui-ngx/src/app/modules/home/components/widget/config/timewindow-style.component.html
  23. 97
      ui-ngx/src/app/modules/home/components/widget/config/timewindow-style.component.ts
  24. 6
      ui-ngx/src/app/modules/home/components/widget/config/widget-config-components.module.ts
  25. 11
      ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog-container.component.ts
  26. 11
      ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog.service.ts
  27. 5
      ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts
  28. 30
      ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts
  29. 5
      ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.html
  30. 7
      ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.scss
  31. 17
      ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.ts
  32. 9
      ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.models.ts
  33. 38
      ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts
  34. 6
      ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.ts
  35. 5
      ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.ts
  36. 1
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/value-card-widget-settings.component.html
  37. 6
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings-panel.component.html
  38. 5
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings-panel.component.ts
  39. 13
      ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts
  40. 1
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.html
  41. 6
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts
  42. 62
      ui-ngx/src/app/modules/home/components/widget/widget-container.component.html
  43. 28
      ui-ngx/src/app/modules/home/components/widget/widget-container.component.scss
  44. 16
      ui-ngx/src/app/modules/home/components/widget/widget-preview.component.ts
  45. 23
      ui-ngx/src/app/modules/home/components/widget/widget.component.ts
  46. 37
      ui-ngx/src/app/modules/home/models/dashboard-component.models.ts
  47. 75
      ui-ngx/src/app/modules/home/models/widget-component.models.ts
  48. 6
      ui-ngx/src/app/shared/components/color-input.component.ts
  49. 13
      ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.ts
  50. 1
      ui-ngx/src/app/shared/components/dialog/material-icons-dialog.component.html
  51. 16
      ui-ngx/src/app/shared/components/dialog/material-icons-dialog.component.ts
  52. 2
      ui-ngx/src/app/shared/components/icon.component.ts
  53. 12
      ui-ngx/src/app/shared/components/json-form/json-form.component.ts
  54. 18
      ui-ngx/src/app/shared/components/markdown.component.ts
  55. 24
      ui-ngx/src/app/shared/components/material-icon-select.component.ts
  56. 9
      ui-ngx/src/app/shared/components/material-icons.component.html
  57. 8
      ui-ngx/src/app/shared/components/material-icons.component.scss
  58. 9
      ui-ngx/src/app/shared/components/material-icons.component.ts
  59. 32
      ui-ngx/src/app/shared/components/time/timewindow.component.html
  60. 17
      ui-ngx/src/app/shared/components/time/timewindow.component.scss
  61. 55
      ui-ngx/src/app/shared/components/time/timewindow.component.ts
  62. 28
      ui-ngx/src/app/shared/models/widget-settings.models.ts
  63. 5
      ui-ngx/src/app/shared/models/widget.models.ts
  64. 13
      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

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 { AuthService } from '@core/auth/auth.service';
import { import {
ColorPickerDialogComponent, ColorPickerDialogComponent,
ColorPickerDialogData ColorPickerDialogData, ColorPickerDialogResult
} from '@shared/components/dialog/color-picker-dialog.component'; } from '@shared/components/dialog/color-picker-dialog.component';
import { import {
MaterialIconsDialogComponent, MaterialIconsDialogComponent,
MaterialIconsDialogData MaterialIconsDialogData, MaterialIconsDialogResult
} from '@shared/components/dialog/material-icons-dialog.component'; } from '@shared/components/dialog/material-icons-dialog.component';
import { ConfirmDialogComponent } from '@shared/components/dialog/confirm-dialog.component'; import { ConfirmDialogComponent } from '@shared/components/dialog/confirm-dialog.component';
import { AlertDialogComponent } from '@shared/components/dialog/alert-dialog.component'; import { AlertDialogComponent } from '@shared/components/dialog/alert-dialog.component';
@ -96,8 +96,8 @@ export class DialogService {
return dialogRef.afterClosed(); return dialogRef.afterClosed();
} }
colorPicker(color: string, colorClearButton = false): Observable<string> { colorPicker(color: string, colorClearButton = false): Observable<ColorPickerDialogResult> {
return this.dialog.open<ColorPickerDialogComponent, ColorPickerDialogData, string>(ColorPickerDialogComponent, return this.dialog.open<ColorPickerDialogComponent, ColorPickerDialogData, ColorPickerDialogResult>(ColorPickerDialogComponent,
{ {
disableClose: true, disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
@ -109,13 +109,14 @@ export class DialogService {
}).afterClosed(); }).afterClosed();
} }
materialIconPicker(icon: string): Observable<string> { materialIconPicker(icon: string, iconClearButton = false): Observable<MaterialIconsDialogResult> {
return this.dialog.open<MaterialIconsDialogComponent, MaterialIconsDialogData, string>(MaterialIconsDialogComponent, return this.dialog.open<MaterialIconsDialogComponent, MaterialIconsDialogData, MaterialIconsDialogResult>(MaterialIconsDialogComponent,
{ {
disableClose: true, disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: { data: {
icon icon,
iconClearButton
}, },
autoFocus: false autoFocus: false
}).afterClosed(); }).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() @NgModule()
export abstract class DynamicComponentModule implements OnDestroy { export abstract class DynamicComponentModule implements OnDestroy {
// eslint-disable-next-line @angular-eslint/contextual-lifecycle
ngOnDestroy(): void { ngOnDestroy(): void {
} }
} }
interface DynamicComponentData<T> {
componentType: Type<T>;
componentModuleRef: NgModuleRef<DynamicComponentModule>;
}
interface DynamicComponentModuleData { interface DynamicComponentModuleData {
moduleRef: NgModuleRef<DynamicComponentModule>; moduleRef: NgModuleRef<DynamicComponentModule>;
moduleType: Type<DynamicComponentModule>; moduleType: Type<DynamicComponentModule>;
@ -48,22 +54,22 @@ interface DynamicComponentModuleData {
}) })
export class DynamicComponentFactoryService { export class DynamicComponentFactoryService {
private dynamicComponentModulesMap = new Map<ComponentFactory<any>, DynamicComponentModuleData>(); private dynamicComponentModulesMap = new Map<Type<any>, DynamicComponentModuleData>();
constructor(private compiler: Compiler, constructor(private compiler: Compiler,
private injector: Injector) { private injector: Injector) {
} }
public createDynamicComponentFactory<T>( public createDynamicComponent<T>(
componentType: Type<T>, componentType: Type<T>,
template: string, template: string,
modules?: Type<any>[], modules?: Type<any>[],
preserveWhitespaces?: boolean, preserveWhitespaces?: boolean,
compileAttempt = 1, compileAttempt = 1,
styles?: string[]): Observable<ComponentFactory<T>> { styles?: string[]): Observable<DynamicComponentData<T>> {
return from(import('@angular/compiler')).pipe( return from(import('@angular/compiler')).pipe(
mergeMap(() => { mergeMap(() => {
const comp = this.createDynamicComponent(componentType, template, preserveWhitespaces, styles); const comp = this._createDynamicComponent(componentType, template, preserveWhitespaces, styles);
let moduleImports: Type<any>[] = [CommonModule]; let moduleImports: Type<any>[] = [CommonModule];
if (modules) { if (modules) {
moduleImports = [...moduleImports, ...modules]; moduleImports = [...moduleImports, ...modules];
@ -82,17 +88,19 @@ export class DynamicComponentFactoryService {
this.compiler.clearCacheFor(module.moduleType); this.compiler.clearCacheFor(module.moduleType);
throw e; throw e;
} }
const factory = moduleRef.componentFactoryResolver.resolveComponentFactory(comp); this.dynamicComponentModulesMap.set(comp, {
this.dynamicComponentModulesMap.set(factory, {
moduleRef, moduleRef,
moduleType: module.moduleType moduleType: module.moduleType
}); });
return factory; return {
componentType: comp,
componentModuleRef: moduleRef
};
}), }),
catchError((error) => { catchError((error) => {
if (compileAttempt === 1) { if (compileAttempt === 1) {
ɵresetCompiledComponents(); ɵresetCompiledComponents();
return this.createDynamicComponentFactory(componentType, template, modules, preserveWhitespaces, ++compileAttempt, styles); return this.createDynamicComponent(componentType, template, modules, preserveWhitespaces, ++compileAttempt, styles);
} else { } else {
throw error; throw error;
} }
@ -102,16 +110,16 @@ export class DynamicComponentFactoryService {
); );
} }
public destroyDynamicComponentFactory<T>(factory: ComponentFactory<T>) { public destroyDynamicComponent<T>(componentType: Type<T>) {
const moduleData = this.dynamicComponentModulesMap.get(factory); const moduleData = this.dynamicComponentModulesMap.get(componentType);
if (moduleData) { if (moduleData) {
moduleData.moduleRef.destroy(); moduleData.moduleRef.destroy();
this.compiler.clearCacheFor(moduleData.moduleType); 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 // noinspection AngularMissingOrInvalidDeclarationInModule
return Component({ return Component({
template, 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 { export function base64toString(b64Encoded: string): string {
return decodeURIComponent(atob(b64Encoded).split('').map((c) => { return decodeURIComponent(atob(b64Encoded).split('').map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)).join(''));
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
}).join(''));
} }
export function objToBase64URI(obj: any): string { export function objToBase64URI(obj: any): string {
@ -190,9 +188,7 @@ export function objToBase64URI(obj: any): string {
} }
export function base64toObj(b64Encoded: string): any { export function base64toObj(b64Encoded: string): any {
const json = decodeURIComponent(atob(b64Encoded).split('').map((c) => { const json = decodeURIComponent(atob(b64Encoded).split('').map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)).join(''));
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
}).join(''));
return JSON.parse(json); return JSON.parse(json);
} }
@ -355,9 +351,7 @@ const SNAKE_CASE_REGEXP = /[A-Z]/g;
export function snakeCase(name: string, separator: string): string { export function snakeCase(name: string, separator: string): string {
separator = separator || '_'; separator = separator || '_';
return name.replace(SNAKE_CASE_REGEXP, (letter, pos) => { return name.replace(SNAKE_CASE_REGEXP, (letter, pos) => (pos ? separator : '') + letter.toLowerCase());
return (pos ? separator : '') + letter.toLowerCase();
});
} }
export function getDescendantProp(obj: any, path: string): any { export function getDescendantProp(obj: any, path: string): any {
@ -381,7 +375,7 @@ export function insertVariable(pattern: string, name: string, value: any): strin
return result; return result;
} }
export function createLabelFromDatasource(datasource: Datasource, pattern: string): string { export const createLabelFromDatasource = (datasource: Datasource, pattern: string): string => {
let label = pattern; let label = pattern;
if (!datasource) { if (!datasource) {
return label; return label;
@ -406,7 +400,9 @@ export function createLabelFromDatasource(datasource: Datasource, pattern: strin
match = varsRegex.exec(pattern); match = varsRegex.exec(pattern);
} }
return label; return label;
} };
export const hasDatasourceLabelsVariables = (pattern: string): boolean => varsRegex.test(pattern) !== null;
export function formattedDataFormDatasourceData(input: DatasourceData[], dataIndex?: number): FormattedData[] { export function formattedDataFormDatasourceData(input: DatasourceData[], dataIndex?: number): FormattedData[] {
return _(input).groupBy(el => el.datasource.entityName + el.datasource.entityType) 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, 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 error = null;
let errorMessage: string; let errorMessage: string;
let timeout = 0; let timeout = 0;

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

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

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> </mat-slide-toggle>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> <div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-material-icon-select asBoxInput <tb-material-icon-select asBoxInput
iconClearButton
[color]="alarmsTableWidgetConfigForm.get('iconColor').value" [color]="alarmsTableWidgetConfigForm.get('iconColor').value"
formControlName="titleIcon"> formControlName="titleIcon">
</tb-material-icon-select> </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 { DataKey, Datasource, WidgetConfig } from '@shared/models/widget.models';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { isUndefined } from '@core/utils'; 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({ @Component({
selector: 'tb-alarms-table-basic-config', selector: 'tb-alarms-table-basic-config',
@ -74,9 +77,7 @@ export class AlarmsTableBasicConfigComponent extends BasicWidgetConfigComponent
} }
protected prepareOutputConfig(config: any): WidgetConfigComponentData { protected prepareOutputConfig(config: any): WidgetConfigComponentData {
this.widgetConfig.config.useDashboardTimewindow = config.timewindowConfig.useDashboardTimewindow; setTimewindowConfig(this.widgetConfig.config, config.timewindowConfig);
this.widgetConfig.config.displayTimewindow = config.timewindowConfig.displayTimewindow;
this.widgetConfig.config.timewindow = config.timewindowConfig.timewindow;
this.widgetConfig.config.alarmFilterConfig = config.alarmFilterConfig; this.widgetConfig.config.alarmFilterConfig = config.alarmFilterConfig;
this.widgetConfig.config.alarmSource = config.datasources[0]; this.widgetConfig.config.alarmSource = config.datasources[0];
this.setColumns(config.columns, this.widgetConfig.config.alarmSource); 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> </mat-slide-toggle>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> <div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-material-icon-select asBoxInput <tb-material-icon-select asBoxInput
iconClearButton
[color]="entitiesTableWidgetConfigForm.get('iconColor').value" [color]="entitiesTableWidgetConfigForm.get('iconColor').value"
formControlName="titleIcon"> formControlName="titleIcon">
</tb-material-icon-select> </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 { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { isUndefined } from '@core/utils'; 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({ @Component({
selector: 'tb-entities-table-basic-config', selector: 'tb-entities-table-basic-config',
@ -93,9 +96,7 @@ export class EntitiesTableBasicConfigComponent extends BasicWidgetConfigComponen
} }
protected prepareOutputConfig(config: any): WidgetConfigComponentData { protected prepareOutputConfig(config: any): WidgetConfigComponentData {
this.widgetConfig.config.useDashboardTimewindow = config.timewindowConfig.useDashboardTimewindow; setTimewindowConfig(this.widgetConfig.config, config.timewindowConfig);
this.widgetConfig.config.displayTimewindow = config.timewindowConfig.displayTimewindow;
this.widgetConfig.config.timewindow = config.timewindowConfig.timewindow;
this.widgetConfig.config.datasources = config.datasources; this.widgetConfig.config.datasources = config.datasources;
this.setColumns(config.columns, this.widgetConfig.config.datasources); this.setColumns(config.columns, this.widgetConfig.config.datasources);
this.widgetConfig.config.actions = config.actions; 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'; } from '@shared/models/widget.models';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; 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 { isUndefined } from '@core/utils';
import { getLabel, setLabel } from '@shared/models/widget-settings.models'; import { getLabel, setLabel } from '@shared/models/widget-settings.models';
@ -80,9 +83,7 @@ export class SimpleCardBasicConfigComponent extends BasicWidgetConfigComponent {
} }
protected prepareOutputConfig(config: any): WidgetConfigComponentData { protected prepareOutputConfig(config: any): WidgetConfigComponentData {
this.widgetConfig.config.useDashboardTimewindow = config.timewindowConfig.useDashboardTimewindow; setTimewindowConfig(this.widgetConfig.config, config.timewindowConfig);
this.widgetConfig.config.displayTimewindow = config.timewindowConfig.displayTimewindow;
this.widgetConfig.config.timewindow = config.timewindowConfig.timewindow;
this.widgetConfig.config.datasources = config.datasources; this.widgetConfig.config.datasources = config.datasources;
setLabel(config.label, this.widgetConfig.config.datasources); setLabel(config.label, this.widgetConfig.config.datasources);
this.widgetConfig.config.actions = config.actions; 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> </mat-slide-toggle>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> <div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-material-icon-select asBoxInput <tb-material-icon-select asBoxInput
iconClearButton
[color]="timeseriesTableWidgetConfigForm.get('iconColor').value" [color]="timeseriesTableWidgetConfigForm.get('iconColor').value"
formControlName="titleIcon"> formControlName="titleIcon">
</tb-material-icon-select> </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 { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { deepClone, isUndefined } from '@core/utils'; 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({ @Component({
selector: 'tb-timeseries-table-basic-config', selector: 'tb-timeseries-table-basic-config',
@ -79,9 +82,7 @@ export class TimeseriesTableBasicConfigComponent extends BasicWidgetConfigCompon
} }
protected prepareOutputConfig(config: any): WidgetConfigComponentData { protected prepareOutputConfig(config: any): WidgetConfigComponentData {
this.widgetConfig.config.useDashboardTimewindow = config.timewindowConfig.useDashboardTimewindow; setTimewindowConfig(this.widgetConfig.config, config.timewindowConfig);
this.widgetConfig.config.displayTimewindow = config.timewindowConfig.displayTimewindow;
this.widgetConfig.config.timewindow = config.timewindowConfig.timewindow;
this.widgetConfig.config.datasources = config.datasources; this.widgetConfig.config.datasources = config.datasources;
this.setColumns(config.columns, this.widgetConfig.config.datasources); this.setColumns(config.columns, this.widgetConfig.config.datasources);
this.widgetConfig.config.actions = config.actions; 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> </mat-form-field>
<tb-css-unit-select fxFlex formControlName="iconSizeUnit"></tb-css-unit-select> <tb-css-unit-select fxFlex formControlName="iconSizeUnit"></tb-css-unit-select>
<tb-material-icon-select asBoxInput <tb-material-icon-select asBoxInput
iconClearButton
[color]="valueCardWidgetConfigForm.get('iconColor').value?.color" [color]="valueCardWidgetConfigForm.get('iconColor').value?.color"
formControlName="icon"> formControlName="icon">
</tb-material-icon-select> </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'; } from '@shared/models/widget.models';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; 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 { formatValue, isDefinedAndNotNull, isUndefined } from '@core/utils';
import { import {
DateFormatProcessor, DateFormatProcessor,
@ -145,9 +148,7 @@ export class ValueCardBasicConfigComponent extends BasicWidgetConfigComponent {
} }
protected prepareOutputConfig(config: any): WidgetConfigComponentData { protected prepareOutputConfig(config: any): WidgetConfigComponentData {
this.widgetConfig.config.useDashboardTimewindow = config.timewindowConfig.useDashboardTimewindow; setTimewindowConfig(this.widgetConfig.config, config.timewindowConfig);
this.widgetConfig.config.displayTimewindow = config.timewindowConfig.displayTimewindow;
this.widgetConfig.config.timewindow = config.timewindowConfig.timewindow;
this.widgetConfig.config.datasources = config.datasources; this.widgetConfig.config.datasources = config.datasources;
this.widgetConfig.config.settings = this.widgetConfig.config.settings || {}; 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> </mat-slide-toggle>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> <div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-material-icon-select asBoxInput <tb-material-icon-select asBoxInput
iconClearButton
[color]="flotWidgetConfigForm.get('iconColor').value" [color]="flotWidgetConfigForm.get('iconColor').value"
formControlName="titleIcon"> formControlName="titleIcon">
</tb-material-icon-select> </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 { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { isUndefined } from '@core/utils'; 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({ @Component({
selector: 'tb-flot-basic-config', selector: 'tb-flot-basic-config',
@ -83,9 +86,7 @@ export class FlotBasicConfigComponent extends BasicWidgetConfigComponent {
} }
protected prepareOutputConfig(config: any): WidgetConfigComponentData { protected prepareOutputConfig(config: any): WidgetConfigComponentData {
this.widgetConfig.config.useDashboardTimewindow = config.timewindowConfig.useDashboardTimewindow; setTimewindowConfig(this.widgetConfig.config, config.timewindowConfig);
this.widgetConfig.config.displayTimewindow = config.timewindowConfig.displayTimewindow;
this.widgetConfig.config.timewindow = config.timewindowConfig.timewindow;
this.widgetConfig.config.datasources = config.datasources; this.widgetConfig.config.datasources = config.datasources;
this.setSeries(config.series, this.widgetConfig.config.datasources); this.setSeries(config.series, this.widgetConfig.config.datasources);
this.widgetConfig.config.actions = config.actions; 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"> <mat-slide-toggle class="mat-slide" formControlName="displayTimewindow">
{{ 'widget-config.display-timewindow' | translate }} {{ 'widget-config.display-timewindow' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
<tb-timewindow asButton="true" <div fxFlex fxLayout="row" fxLayoutAlign="space-between center">
strokedButton <tb-timewindow asButton="true"
noMargin strokedButton
isEdit="true" noMargin
alwaysDisplayTypePrefix isEdit="true"
[historyOnly]="onlyHistoryTimewindow" alwaysDisplayTypePrefix
quickIntervalOnly="{{ widgetType === widgetTypes.latest }}" [historyOnly]="onlyHistoryTimewindow"
aggregation="{{ widgetType === widgetTypes.timeseries }}" quickIntervalOnly="{{ widgetType === widgetTypes.latest }}"
formControlName="timewindow"></tb-timewindow> aggregation="{{ widgetType === widgetTypes.timeseries }}"
formControlName="timewindow"></tb-timewindow>
<tb-timewindow-style formControlName="timewindowStyle"
[previewValue]="timewindowConfig.get('timewindow').value">
</tb-timewindow-style>
</div>
</div> </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 { TranslateService } from '@ngx-translate/core';
import { coerceBoolean } from '@shared/decorators/coercion'; import { coerceBoolean } from '@shared/decorators/coercion';
import { isDefined } from '@core/utils'; import { isDefined } from '@core/utils';
import { TimewindowStyle } from '@shared/models/widget-settings.models';
export interface TimewindowConfigData { export interface TimewindowConfigData {
useDashboardTimewindow: boolean; useDashboardTimewindow: boolean;
displayTimewindow: boolean; displayTimewindow: boolean;
timewindow: Timewindow; timewindow: Timewindow;
timewindowStyle: TimewindowStyle;
} }
export const getTimewindowConfig = (config: WidgetConfig): TimewindowConfigData => ({ export const getTimewindowConfig = (config: WidgetConfig): TimewindowConfigData => ({
@ -34,9 +36,17 @@ export const getTimewindowConfig = (config: WidgetConfig): TimewindowConfigData
config.useDashboardTimewindow : true, config.useDashboardTimewindow : true,
displayTimewindow: isDefined(config.displayTimewindow) ? displayTimewindow: isDefined(config.displayTimewindow) ?
config.displayTimewindow : true, 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({ @Component({
selector: 'tb-timewindow-config-panel', selector: 'tb-timewindow-config-panel',
templateUrl: './timewindow-config-panel.component.html', templateUrl: './timewindow-config-panel.component.html',
@ -78,7 +88,8 @@ export class TimewindowConfigPanelComponent implements ControlValueAccessor, OnI
this.timewindowConfig = this.fb.group({ this.timewindowConfig = this.fb.group({
useDashboardTimewindow: [null, []], useDashboardTimewindow: [null, []],
displayTimewindow: [null, []], displayTimewindow: [null, []],
timewindow: [null, []] timewindow: [null, []],
timewindowStyle: [null, []]
}); });
this.timewindowConfig.valueChanges.subscribe( this.timewindowConfig.valueChanges.subscribe(
(val) => this.propagateChange(val) (val) => this.propagateChange(val)
@ -86,6 +97,9 @@ export class TimewindowConfigPanelComponent implements ControlValueAccessor, OnI
this.timewindowConfig.get('useDashboardTimewindow').valueChanges.subscribe(() => { this.timewindowConfig.get('useDashboardTimewindow').valueChanges.subscribe(() => {
this.updateTimewindowConfigEnabledState(); this.updateTimewindowConfigEnabledState();
}); });
this.timewindowConfig.get('displayTimewindow').valueChanges.subscribe(() => {
this.updateTimewindowConfigEnabledState();
});
} }
writeValue(data?: TimewindowConfigData): void { writeValue(data?: TimewindowConfigData): void {
@ -112,12 +126,19 @@ export class TimewindowConfigPanelComponent implements ControlValueAccessor, OnI
private updateTimewindowConfigEnabledState() { private updateTimewindowConfigEnabledState() {
const useDashboardTimewindow: boolean = this.timewindowConfig.get('useDashboardTimewindow').value; const useDashboardTimewindow: boolean = this.timewindowConfig.get('useDashboardTimewindow').value;
const displayTimewindow: boolean = this.timewindowConfig.get('displayTimewindow').value;
if (useDashboardTimewindow) { if (useDashboardTimewindow) {
this.timewindowConfig.get('displayTimewindow').disable({emitEvent: false}); this.timewindowConfig.get('displayTimewindow').disable({emitEvent: false});
this.timewindowConfig.get('timewindow').disable({emitEvent: false}); this.timewindowConfig.get('timewindow').disable({emitEvent: false});
this.timewindowConfig.get('timewindowStyle').disable({emitEvent: false});
} else { } else {
this.timewindowConfig.get('displayTimewindow').enable({emitEvent: false}); this.timewindowConfig.get('displayTimewindow').enable({emitEvent: false});
this.timewindowConfig.get('timewindow').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 { WidgetSettingsComponent } from '@home/components/widget/config/widget-settings.component';
import { TimewindowConfigPanelComponent } from '@home/components/widget/config/timewindow-config-panel.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 { 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({ @NgModule({
declarations: declarations:
@ -43,6 +45,8 @@ import { WidgetSettingsCommonModule } from '@home/components/widget/lib/settings
DatasourcesComponent, DatasourcesComponent,
EntityAliasSelectComponent, EntityAliasSelectComponent,
FilterSelectComponent, FilterSelectComponent,
TimewindowStyleComponent,
TimewindowStylePanelComponent,
TimewindowConfigPanelComponent, TimewindowConfigPanelComponent,
WidgetSettingsComponent WidgetSettingsComponent
], ],
@ -62,6 +66,8 @@ import { WidgetSettingsCommonModule } from '@home/components/widget/lib/settings
DatasourcesComponent, DatasourcesComponent,
EntityAliasSelectComponent, EntityAliasSelectComponent,
FilterSelectComponent, FilterSelectComponent,
TimewindowStyleComponent,
TimewindowStylePanelComponent,
TimewindowConfigPanelComponent, TimewindowConfigPanelComponent,
WidgetSettingsComponent, WidgetSettingsComponent,
WidgetSettingsCommonModule WidgetSettingsCommonModule

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

@ -20,8 +20,8 @@ import {
ComponentFactory, ComponentFactory,
ComponentRef, HostBinding, ComponentRef, HostBinding,
Inject, Inject,
Injector, Injector, NgModuleRef,
OnDestroy, OnDestroy, Type,
ViewContainerRef ViewContainerRef
} from '@angular/core'; } from '@angular/core';
import { DialogComponent } from '@shared/components/dialog.component'; import { DialogComponent } from '@shared/components/dialog.component';
@ -35,11 +35,13 @@ import {
} from '@home/components/widget/dialog/custom-dialog.component'; } from '@home/components/widget/dialog/custom-dialog.component';
import { DialogService } from '@core/services/dialog.service'; import { DialogService } from '@core/services/dialog.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { DynamicComponentModule } from '@core/services/dynamic-component-factory.service';
export interface CustomDialogContainerData { export interface CustomDialogContainerData {
controller: (instance: CustomDialogComponent) => void; controller: (instance: CustomDialogComponent) => void;
data?: any; data?: any;
customComponentFactory: ComponentFactory<CustomDialogComponent>; customComponentType: Type<CustomDialogComponent>;
customComponentModuleRef: NgModuleRef<DynamicComponentModule>;
} }
@Component({ @Component({
@ -77,7 +79,8 @@ export class CustomDialogContainerComponent extends DialogComponent<CustomDialog
}] }]
}); });
try { 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) { } catch (e: any) {
let message; let message;
if (e.message?.startsWith('NG0')) { 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() @Injectable()
export class CustomDialogService { export class CustomDialogService {
private customModules: Array<Type<any>> private customModules: Array<Type<any>>;
constructor( constructor(
private translate: TranslateService, private translate: TranslateService,
@ -56,12 +56,13 @@ export class CustomDialogService {
if (Array.isArray(this.customModules)) { if (Array.isArray(this.customModules)) {
modules.push(...this.customModules); modules.push(...this.customModules);
} }
return this.dynamicComponentFactoryService.createDynamicComponentFactory( return this.dynamicComponentFactoryService.createDynamicComponent(
class CustomDialogComponentInstance extends CustomDialogComponent {}, template, modules).pipe( class CustomDialogComponentInstance extends CustomDialogComponent {}, template, modules).pipe(
mergeMap((factory) => { mergeMap((componentData) => {
const dialogData: CustomDialogContainerData = { const dialogData: CustomDialogContainerData = {
controller, controller,
customComponentFactory: factory, customComponentType: componentData.componentType,
customComponentModuleRef: componentData.componentModuleRef,
data data
}; };
let dialogConfig: MatDialogConfig = { let dialogConfig: MatDialogConfig = {
@ -76,7 +77,7 @@ export class CustomDialogService {
CustomDialogContainerComponent, CustomDialogContainerComponent,
dialogConfig).afterClosed().pipe( dialogConfig).afterClosed().pipe(
tap(() => { 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 { 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 { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { IDynamicWidgetComponent, WidgetContext } from '@home/models/widget-component.models'; 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(UntypedFormBuilder) public fb: UntypedFormBuilder,
@TbInject(Injector) public readonly $injector: Injector, @TbInject(Injector) public readonly $injector: Injector,
@TbInject('widgetContext') public readonly ctx: WidgetContext, @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); super(store);
this.ctx.$injector = $injector; this.ctx.$injector = $injector;
this.ctx.deviceService = $injector.get(DeviceService); 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 { IWidgetSubscription } from '@core/api/widget-api.models';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { import { deepClone, hashCode, isDefined, isDefinedAndNotNull, isNumber, isObject, isUndefined } from '@core/utils';
createLabelFromDatasource,
deepClone,
hashCode,
isDefined,
isDefinedAndNotNull,
isNumber,
isObject,
isUndefined
} from '@core/utils';
import cssjs from '@core/css/css'; import cssjs from '@core/css/css';
import { sortItems } from '@shared/models/page/page-link'; import { sortItems } from '@shared/models/page/page-link';
import { Direction } from '@shared/models/page/sort-order'; import { Direction } from '@shared/models/page/sort-order';
@ -194,8 +185,6 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
private subscription: IWidgetSubscription; private subscription: IWidgetSubscription;
private widgetResize$: ResizeObserver; private widgetResize$: ResizeObserver;
private alarmsTitlePattern: string;
private displayActivity = false; private displayActivity = false;
private displayDetails = true; private displayDetails = true;
public allowAcknowledgment = true; public allowAcknowledgment = true;
@ -322,7 +311,6 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
} }
public onDataUpdated() { public onDataUpdated() {
this.updateTitle(true);
this.alarmsDatasource.updateAlarms(); this.alarmsDatasource.updateAlarms();
this.clearCache(); this.clearCache();
this.ctx.detectChanges(); this.ctx.detectChanges();
@ -342,13 +330,11 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
this.allowAssign = isDefined(this.settings.allowAssign) ? this.settings.allowAssign : true; this.allowAssign = isDefined(this.settings.allowAssign) ? this.settings.allowAssign : true;
if (this.settings.alarmsTitle && this.settings.alarmsTitle.length) { 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 { } 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; this.enableSelection = isDefined(this.settings.enableSelection) ? this.settings.enableSelection : true;
if (!this.allowAcknowledgment && !this.allowClear) { if (!this.allowAcknowledgment && !this.allowClear) {
this.enableSelection = false; this.enableSelection = false;
@ -394,16 +380,6 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
$(this.elementRef.nativeElement).addClass(namespace); $(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() { private updateAlarmSource() {
if (this.enableSelection) { 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-panel" [class]="this.layout" [style]="backgroundStyle">
<div class="tb-value-card-overlay" [style]="overlayStyle"></div> <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-container [ngSwitch]="layout">
<ng-template [ngSwitchCase]="valueCardLayout.square"> <ng-template [ngSwitchCase]="valueCardLayout.square">
<ng-container *ngTemplateOutlet="iconWithLabelTpl"></ng-container> <ng-container *ngTemplateOutlet="iconWithLabelTpl"></ng-container>
@ -60,7 +63,7 @@
<tb-icon *ngIf="showIcon" [style]="iconStyle" [style.color]="iconColor.color">{{ icon }}</tb-icon> <tb-icon *ngIf="showIcon" [style]="iconStyle" [style.color]="iconColor.color">{{ icon }}</tb-icon>
</ng-template> </ng-template>
<ng-template #labelTpl> <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>
<ng-template #dateTpl> <ng-template #dateTpl>
<div *ngIf="showDate" [style]="dateStyle" [style.color]="dateColor.color">{{ dateFormat.formatted }}</div> <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; bottom: 12px;
right: 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 { .tb-value-card-icon-row {
display: flex; display: flex;
flex-direction: row; 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. /// 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 { WidgetContext } from '@home/models/widget-component.models';
import { formatValue, isDefinedAndNotNull } from '@core/utils'; import { formatValue, isDefinedAndNotNull } from '@core/utils';
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
@ -31,6 +31,7 @@ import {
} from '@shared/models/widget-settings.models'; } from '@shared/models/widget-settings.models';
import { valueCardDefaultSettings, ValueCardLayout, ValueCardWidgetSettings } from './value-card-widget.models'; import { valueCardDefaultSettings, ValueCardLayout, ValueCardWidgetSettings } from './value-card-widget.models';
import { WidgetComponent } from '@home/components/widget/widget.component'; import { WidgetComponent } from '@home/components/widget/widget.component';
import { Observable } from 'rxjs';
@Component({ @Component({
selector: 'tb-value-card-widget', selector: 'tb-value-card-widget',
@ -46,6 +47,9 @@ export class ValueCardWidgetComponent implements OnInit {
@Input() @Input()
ctx: WidgetContext; ctx: WidgetContext;
@Input()
widgetTitlePanel: TemplateRef<any>;
layout: ValueCardLayout; layout: ValueCardLayout;
showIcon = true; showIcon = true;
icon = ''; icon = '';
@ -53,7 +57,7 @@ export class ValueCardWidgetComponent implements OnInit {
iconColor: ColorProcessor; iconColor: ColorProcessor;
showLabel = true; showLabel = true;
label = ''; label$: Observable<string>;
labelStyle: ComponentStyle = {}; labelStyle: ComponentStyle = {};
labelColor: ColorProcessor; labelColor: ColorProcessor;
@ -102,15 +106,16 @@ export class ValueCardWidgetComponent implements OnInit {
this.iconColor = ColorProcessor.fromSettings(this.settings.iconColor); this.iconColor = ColorProcessor.fromSettings(this.settings.iconColor);
this.showLabel = this.settings.showLabel; this.showLabel = this.settings.showLabel;
this.label = getLabel(this.ctx.datasources); const label = getLabel(this.ctx.datasources);
this.labelStyle = textStyle(this.settings.labelFont, '1.5', '0.25px'); this.label$ = this.ctx.registerLabelPattern('valueCardLabel', label);
this.labelStyle = textStyle(this.settings.labelFont, '0.25px');
this.labelColor = ColorProcessor.fromSettings(this.settings.labelColor); 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.valueColor = ColorProcessor.fromSettings(this.settings.valueColor);
this.showDate = this.settings.showDate; this.showDate = this.settings.showDate;
this.dateFormat = DateFormatProcessor.fromSettings(this.ctx.$injector, this.settings.dateFormat); 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.dateColor = ColorProcessor.fromSettings(this.settings.dateColor);
this.backgroundStyle = backgroundStyle(this.settings.background); 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, size: 16,
sizeUnit: 'px', sizeUnit: 'px',
style: 'normal', style: 'normal',
weight: '500' weight: '500',
lineHeight: '1.5'
}, },
labelColor: constantColor('rgba(0, 0, 0, 0.87)'), labelColor: constantColor('rgba(0, 0, 0, 0.87)'),
showIcon: true, showIcon: true,
@ -102,7 +103,8 @@ export const valueCardDefaultSettings = (horizontal: boolean): ValueCardWidgetSe
size: 52, size: 52,
sizeUnit: 'px', sizeUnit: 'px',
style: 'normal', style: 'normal',
weight: '500' weight: '500',
lineHeight: '100%'
}, },
valueColor: constantColor('rgba(0, 0, 0, 0.87)'), valueColor: constantColor('rgba(0, 0, 0, 0.87)'),
showDate: true, showDate: true,
@ -112,7 +114,8 @@ export const valueCardDefaultSettings = (horizontal: boolean): ValueCardWidgetSe
size: 12, size: 12,
sizeUnit: 'px', sizeUnit: 'px',
style: 'normal', style: 'normal',
weight: '500' weight: '500',
lineHeight: '1.33'
}, },
dateColor: constantColor('rgba(0, 0, 0, 0.38)'), dateColor: constantColor('rgba(0, 0, 0, 0.38)'),
background: { 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 { IWidgetSubscription } from '@core/api/widget-api.models';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { import { deepClone, hashCode, isDefined, isNumber, isObject, isUndefined } from '@core/utils';
createLabelFromDatasource,
deepClone,
hashCode,
isDefined,
isNumber,
isObject,
isUndefined
} from '@core/utils';
import cssjs from '@core/css/css'; import cssjs from '@core/css/css';
import { CollectionViewer, DataSource } from '@angular/cdk/collections'; import { CollectionViewer, DataSource } from '@angular/cdk/collections';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
@ -164,8 +156,6 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
private subscription: IWidgetSubscription; private subscription: IWidgetSubscription;
private widgetResize$: ResizeObserver; private widgetResize$: ResizeObserver;
private entitiesTitlePattern: string;
private defaultPageSize = 10; private defaultPageSize = 10;
private defaultSortOrder = 'entityName'; private defaultSortOrder = 'entityName';
@ -266,7 +256,6 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
} }
public onDataUpdated() { public onDataUpdated() {
this.updateTitle(true);
this.entityDatasource.dataUpdated(); this.entityDatasource.dataUpdated();
this.clearCache(); this.clearCache();
this.ctx.detectChanges(); this.ctx.detectChanges();
@ -281,16 +270,15 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
this.setCellButtonAction = !!this.ctx.actionsApi.getActionDescriptors('actionCellButton').length; 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) { 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 { } 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.searchAction.show = isDefined(this.settings.enableSearch) ? this.settings.enableSearch : true;
this.displayPagination = isDefined(this.settings.displayPagination) ? this.settings.displayPagination : true; this.displayPagination = isDefined(this.settings.displayPagination) ? this.settings.displayPagination : true;
this.enableStickyHeader = isDefined(this.settings.enableStickyHeader) ? this.settings.enableStickyHeader : 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); $(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() { private updateDatasources() {
const displayEntityName = isDefined(this.settings.displayEntityName) ? this.settings.displayEntityName : true; const displayEntityName = isDefined(this.settings.displayEntityName) ? this.settings.displayEntityName : true;
@ -498,14 +476,12 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
overlayRef.dispose(); overlayRef.dispose();
}); });
const columns: DisplayColumn[] = this.columns.map(column => { const columns: DisplayColumn[] = this.columns.map(column => ({
return {
title: column.title, title: column.title,
def: column.def, def: column.def,
display: this.displayedColumns.indexOf(column.def) > -1, display: this.displayedColumns.indexOf(column.def) > -1,
selectable: this.columnSelectionAvailability[column.def] selectable: this.columnSelectionAvailability[column.def]
}; }));
});
const providers: StaticProvider[] = [ 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() { private initializeConfig() {
if (this.settings.widgetTitle && this.settings.widgetTitle.length) { if (this.settings.widgetTitle && this.settings.widgetTitle.length) {
const title = createLabelFromDatasource(this.datasource, this.settings.widgetTitle); this.ctx.widgetTitle = this.settings.widgetTitle;
this.ctx.widgetTitle = this.utils.customTranslation(title, title);
} else {
this.ctx.widgetTitle = this.ctx.widgetConfig.title;
} }
if (this.settings.labelValue && this.settings.labelValue.length) { if (this.settings.labelValue && this.settings.labelValue.length) {
const label = createLabelFromDatasource(this.datasource, this.settings.labelValue); const label = createLabelFromDatasource(this.datasource, this.settings.labelValue);
this.labelValue = this.utils.customTranslation(label, label); 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() { private initializeConfig() {
if (this.settings.widgetTitle && this.settings.widgetTitle.length) { if (this.settings.widgetTitle && this.settings.widgetTitle.length) {
const titlePatternText = this.utils.customTranslation(this.settings.widgetTitle, this.settings.widgetTitle); this.ctx.widgetTitle = this.settings.widgetTitle;
this.ctx.widgetTitle = createLabelFromDatasource(this.datasources[0], titlePatternText);
} else {
this.ctx.widgetTitle = this.ctx.widgetConfig.title;
} }
this.settings.groupTitle = this.settings.groupTitle || '${entityName}'; this.settings.groupTitle = this.settings.groupTitle || '${entityName}';

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

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

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

@ -72,6 +72,12 @@
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </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> <mat-divider></mat-divider>
<div class="tb-form-row no-border no-padding font-preview"> <div class="tb-form-row no-border no-padding font-preview">
<div class="fixed-title-width" translate>widgets.widget-font.preview</div> <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'), []], sizeUnit: [(this.font?.sizeUnit || 'px'), []],
family: [this.font?.family, []], family: [this.font?.family, []],
weight: [this.font?.weight, []], weight: [this.font?.weight, []],
style: [this.font?.style, []] style: [this.font?.style, []],
lineHeight: [this.font?.lineHeight, []]
} }
); );
this.updatePreviewStyle(this.font); this.updatePreviewStyle(this.font);
@ -146,7 +147,7 @@ export class FontSettingsPanelComponent extends PageComponent implements OnInit
} }
private updatePreviewStyle(font: Font) { 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 { 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); this.widgetService.deleteWidgetInfoFromCache(bundleAlias, widgetTypeAlias, isSystem);
} }
@ -362,13 +362,14 @@ export class WidgetComponentService {
return of(resolvedModules); return of(resolvedModules);
} else { } else {
this.registerWidgetSettingsForms(widgetInfo, resolvedModules.factories); this.registerWidgetSettingsForms(widgetInfo, resolvedModules.factories);
return this.dynamicComponentFactoryService.createDynamicComponentFactory( return this.dynamicComponentFactoryService.createDynamicComponent(
class DynamicWidgetComponentInstance extends DynamicWidgetComponent {}, class DynamicWidgetComponentInstance extends DynamicWidgetComponent {},
widgetInfo.templateHtml, widgetInfo.templateHtml,
resolvedModules.modules resolvedModules.modules
).pipe( ).pipe(
map((factory) => { map((componentData) => {
widgetInfo.componentFactory = factory; widgetInfo.componentType = componentData.componentType;
widgetInfo.componentModuleRef = componentData.componentModuleRef;
return null; return null;
}), }),
catchError(e => { catchError(e => {
@ -546,8 +547,8 @@ export class WidgetComponentService {
if (isUndefined(result.typeParameters.previewHeight)) { if (isUndefined(result.typeParameters.previewHeight)) {
result.typeParameters.previewHeight = '70%'; result.typeParameters.previewHeight = '70%';
} }
if (isUndefined(result.typeParameters.absoluteHeader)) { if (isUndefined(result.typeParameters.embedTitlePanel)) {
result.typeParameters.absoluteHeader = false; result.typeParameters.embedTitlePanel = false;
} }
if (isFunction(widgetTypeInstance.actionSources)) { if (isFunction(widgetTypeInstance.actionSources)) {
result.actionSources = 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"> <input matInput formControlName="iconSize">
</mat-form-field> </mat-form-field>
<tb-material-icon-select asBoxInput <tb-material-icon-select asBoxInput
iconClearButton
[color]="widgetSettings.get('iconColor').value" [color]="widgetSettings.get('iconColor').value"
formControlName="titleIcon"> formControlName="titleIcon">
</tb-material-icon-select> </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({ this.dataSettings.addControl('timewindowConfig', this.fb.control({
useDashboardTimewindow: true, useDashboardTimewindow: true,
displayTimewindow: true, displayTimewindow: true,
timewindow: null timewindow: null,
timewindowStyle: null
})); }));
if (this.widgetType === widgetType.alarm) { if (this.widgetType === widgetType.alarm) {
this.dataSettings.addControl('alarmFilterConfig', this.fb.control(null)); this.dataSettings.addControl('alarmFilterConfig', this.fb.control(null));
@ -517,7 +518,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
useDashboardTimewindow, useDashboardTimewindow,
displayTimewindow: isDefined(config.displayTimewindow) ? displayTimewindow: isDefined(config.displayTimewindow) ?
config.displayTimewindow : true, config.displayTimewindow : true,
timewindow: config.timewindow timewindow: config.timewindow,
timewindowStyle: config.timewindowStyle
}, {emitEvent: false}); }, {emitEvent: false});
} }
if (this.modelValue.isDataEnabled) { if (this.modelValue.isDataEnabled) {

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

@ -32,34 +32,13 @@
(click)="onClicked($event)" (click)="onClicked($event)"
(contextmenu)="onContextMenu($event)"> (contextmenu)="onContextMenu($event)">
<div *ngIf="!!widgetComponent.widgetContext?.inited" <div *ngIf="!!widgetComponent.widgetContext?.inited"
class="tb-widget-header" class="tb-widget-header">
[class]="{'tb-widget-header-absolute': widgetComponent.widgetContext?.absoluteHeader}"> <ng-container *ngIf="!widgetComponent.widgetContext?.embedTitlePanel">
<div *ngIf="widget.showWidgetTitlePanel" <ng-container *ngTemplateOutlet="widgetTitlePanel"></ng-container>
class="tb-widget-title"> </ng-container>
<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>
<div *ngIf="widget.showWidgetActions" <div *ngIf="widget.showWidgetActions"
class="tb-widget-actions" 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()"> (mousedown)="$event.stopPropagation()">
<button mat-icon-button *ngFor="let action of widget.customHeaderActions" <button mat-icon-button *ngFor="let action of widget.customHeaderActions"
[fxShow]="!isEdit" [fxShow]="!isEdit"
@ -109,7 +88,36 @@
<tb-widget #widgetComponent <tb-widget #widgetComponent
[dashboardWidget]="widget" [dashboardWidget]="widget"
[isEdit]="isEdit" [isEdit]="isEdit"
[isMobile]="isMobile"> [isMobile]="isMobile"
[widgetTitlePanel]="widgetTitlePanel">
</tb-widget> </tb-widget>
</div> </div>
</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; flex-direction: row;
place-content: flex-start space-between; place-content: flex-start space-between;
align-items: flex-start; align-items: flex-start;
&-absolute {
position: absolute;
top: 0;
right: 0;
left: 0;
z-index: 1;
}
} }
.tb-widget-title { .tb-widget-title {
@ -59,23 +52,24 @@ div.tb-widget {
tb-timewindow { tb-timewindow {
font-size: 14px; font-size: 14px;
opacity: .85; opacity: .85;
margin: 0;
} }
.title { .title-row {
display: flex;
flex-direction: row;
place-content: center flex-start;
align-items: center;
gap: 4px;
width: 100%; width: 100%;
}
.title {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap;
line-height: 24px; line-height: 24px;
letter-spacing: .01em; letter-spacing: normal;
margin: 0; 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() { private loadPreviewWidget() {
const widget = deepClone(this.widget); if (this.widget) {
widget.sizeX = 24; const widget = deepClone(this.widget);
widget.sizeY = this.widget.sizeY * 2; widget.sizeX = 24;
widget.row = 0; widget.sizeY = this.widget.sizeY * 2;
widget.col = 0; widget.row = 0;
widget.config = this.widgetConfig; widget.col = 0;
this.widgets = [widget]; 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, Optional,
Renderer2, Renderer2,
SimpleChanges, SimpleChanges,
TemplateRef,
Type, Type,
ViewChild, ViewChild,
ViewContainerRef, ViewContainerRef,
@ -125,6 +126,9 @@ import { IModulesMap } from '@modules/common/modules-map.models';
}) })
export class WidgetComponent extends PageComponent implements OnInit, AfterViewInit, OnChanges, OnDestroy { export class WidgetComponent extends PageComponent implements OnInit, AfterViewInit, OnChanges, OnDestroy {
@Input()
widgetTitlePanel: TemplateRef<any>;
@Input() @Input()
isEdit: boolean; isEdit: boolean;
@ -308,9 +312,6 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
this.loadFromWidgetInfo(); this.loadFromWidgetInfo();
} }
); );
setTimeout(() => {
this.dashboardWidget.updateWidgetParams();
}, 0);
const noDataDisplayMessage = this.widget.config.noDataDisplayMessage; const noDataDisplayMessage = this.widget.config.noDataDisplayMessage;
if (isNotEmptyStr(noDataDisplayMessage)) { if (isNotEmptyStr(noDataDisplayMessage)) {
@ -387,7 +388,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
this.handleWidgetException(e); this.handleWidgetException(e);
} }
} }
this.widgetContext.destroyed = true; this.widgetContext.destroy();
this.destroyDynamicWidgetComponent(); this.destroyDynamicWidgetComponent();
} }
} }
@ -409,7 +410,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
elem.classList.add(this.widgetContext.widgetNamespace); elem.classList.add(this.widgetContext.widgetNamespace);
this.widgetType = this.widgetInfo.widgetTypeFunction; this.widgetType = this.widgetInfo.widgetTypeFunction;
this.typeParameters = this.widgetInfo.typeParameters; this.typeParameters = this.widgetInfo.typeParameters;
this.widgetContext.absoluteHeader = this.typeParameters.absoluteHeader; this.widgetContext.embedTitlePanel = this.typeParameters.embedTitlePanel;
if (!this.widgetType) { if (!this.widgetType) {
this.widgetTypeInstance = {}; this.widgetTypeInstance = {};
@ -480,6 +481,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
} }
if (!this.widgetContext.inited && this.isReady()) { if (!this.widgetContext.inited && this.isReady()) {
this.widgetContext.inited = true; this.widgetContext.inited = true;
this.dashboardWidget.updateWidgetParams();
this.widgetContext.detectContainerChanges(); this.widgetContext.detectContainerChanges();
if (this.cafs.init) { if (this.cafs.init) {
this.cafs.init(); this.cafs.init();
@ -493,7 +495,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
if (this.dataUpdatePending) { if (this.dataUpdatePending) {
this.widgetTypeInstance.onDataUpdated(); this.widgetTypeInstance.onDataUpdated();
setTimeout(() => { setTimeout(() => {
this.dashboardWidget.updateCustomHeaderActions(true); this.dashboardWidget.updateParamsFromData(true);
}, 0); }, 0);
this.dataUpdatePending = false; this.dataUpdatePending = false;
} }
@ -735,6 +737,10 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
{ {
provide: 'errorMessages', provide: 'errorMessages',
useValue: this.errorMessages useValue: this.errorMessages
},
{
provide: 'widgetTitlePanel',
useValue: this.widgetTitlePanel
} }
], ],
parent: this.injector parent: this.injector
@ -745,7 +751,8 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
this.widgetContext.$containerParent = $(containerElement); this.widgetContext.$containerParent = $(containerElement);
try { 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(); this.cd.detectChanges();
} catch (e) { } catch (e) {
if (this.dynamicWidgetComponentRef) { if (this.dynamicWidgetComponentRef) {
@ -845,7 +852,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
if (this.widgetInstanceInited) { if (this.widgetInstanceInited) {
this.widgetTypeInstance.onDataUpdated(); this.widgetTypeInstance.onDataUpdated();
setTimeout(() => { setTimeout(() => {
this.dashboardWidget.updateCustomHeaderActions(true); this.dashboardWidget.updateParamsFromData(true);
}, 0); }, 0);
} else { } else {
this.dataUpdatePending = true; 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 { enumerable } from '@shared/decorators/enumerable';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { TbPopoverComponent } from '@shared/components/popover.component'; 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 { export interface WidgetsData {
widgets: Array<Widget>; widgets: Array<Widget>;
@ -331,8 +331,7 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget {
margin: string; margin: string;
borderRadius: string; borderRadius: string;
title: string; title$: Observable<string>;
customTranslatedTitle: string;
titleTooltip: string; titleTooltip: string;
showTitle: boolean; showTitle: boolean;
titleStyle: ComponentStyle; titleStyle: ComponentStyle;
@ -431,30 +430,23 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget {
this.margin = this.widget.config.margin || '0px'; this.margin = this.widget.config.margin || '0px';
this.borderRadius = this.widget.config.borderRadius; 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.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('widgetTitle', title);
this.titleTooltip = isDefined(this.widgetContext.widgetTitleTooltip) this.titleTooltip = isDefined(this.widgetContext.widgetTitleTooltip)
&& this.widgetContext.widgetTitleTooltip.length ? this.widgetContext.widgetTitleTooltip : this.widget.config.titleTooltip; && this.widgetContext.widgetTitleTooltip.length ? this.widgetContext.widgetTitleTooltip : this.widget.config.titleTooltip;
this.titleTooltip = this.dashboard.utils.customTranslation(this.titleTooltip, this.titleTooltip); this.titleTooltip = this.dashboard.utils.customTranslation(this.titleTooltip, this.titleTooltip);
this.showTitle = isDefined(this.widget.config.showTitle) ? this.widget.config.showTitle : true; 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) { if (this.widget.config.titleColor) {
this.titleStyle.color = this.widget.config.titleColor; this.titleStyle.color = this.widget.config.titleColor;
} }
this.titleIcon = isDefined(this.widget.config.titleIcon) ? this.widget.config.titleIcon : ''; this.titleIcon = isDefined(this.widget.config.titleIcon) ? this.widget.config.titleIcon : '';
this.showTitleIcon = isDefined(this.widget.config.showTitleIcon) ? this.widget.config.showTitleIcon : false; 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) { if (this.widget.config.iconColor) {
this.titleIconStyle.color = 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.dropShadow = isDefined(this.widget.config.dropShadow) ? this.widget.config.dropShadow : true;
this.enableFullscreen = isDefined(this.widget.config.enableFullscreen) ? this.widget.config.enableFullscreen : 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.showWidgetActions = !this.widgetContext.hideTitlePanel;
this.updateCustomHeaderActions(); this.updateParamsFromData();
this.widgetActions = this.widgetContext.widgetActions ? this.widgetContext.widgetActions : []; this.widgetActions = this.widgetContext.widgetActions ? this.widgetContext.widgetActions : [];
if (detectChanges) { if (detectChanges) {
this.widgetContext.detectContainerChanges(); 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>; let customHeaderActions: Array<WidgetHeaderAction>;
if (this.widgetContext.customHeaderActions) { if (this.widgetContext.customHeaderActions) {
let data: FormattedData[] = []; let data: FormattedData[] = [];
@ -517,10 +517,9 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget {
} }
if (!isEqual(this.customHeaderActions, customHeaderActions)) { if (!isEqual(this.customHeaderActions, customHeaderActions)) {
this.customHeaderActions = customHeaderActions; this.customHeaderActions = customHeaderActions;
if (detectChanges) { return true;
this.widgetContext.detectContainerChanges();
}
} }
return false;
} }
private filterCustomHeaderAction(action: WidgetHeaderAction, data: FormattedData[]): boolean { private filterCustomHeaderAction(action: WidgetHeaderAction, data: FormattedData[]): boolean {

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

@ -45,13 +45,13 @@ import {
WidgetActionsApi, WidgetActionsApi,
WidgetSubscriptionApi WidgetSubscriptionApi
} from '@core/api/widget-api.models'; } 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 { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { RafService } from '@core/services/raf.service'; import { RafService } from '@core/services/raf.service';
import { WidgetTypeId } from '@shared/models/id/widget-type-id'; import { WidgetTypeId } from '@shared/models/id/widget-type-id';
import { TenantId } from '@shared/models/id/tenant-id'; import { TenantId } from '@shared/models/id/tenant-id';
import { WidgetLayout } from '@shared/models/dashboard.models'; 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 { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { import {
@ -87,10 +87,12 @@ import * as RxJS from 'rxjs';
import * as RxJSOperators from 'rxjs/operators'; import * as RxJSOperators from 'rxjs/operators';
import { TbPopoverComponent } from '@shared/components/popover.component'; import { TbPopoverComponent } from '@shared/components/popover.component';
import { EntityId } from '@shared/models/id/entity-id'; 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 { MillisecondsToTimeStringPipe, TelemetrySubscriber } from '@app/shared/public-api';
import { UserId } from '@shared/models/id/user-id'; import { UserId } from '@shared/models/id/user-id';
import { UserSettingsService } from '@core/http/user-settings.service'; 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 { export interface IWidgetAction {
name: string; name: string;
@ -200,6 +202,8 @@ export class WidgetContext {
subscriptions: {[id: string]: IWidgetSubscription} = {}; subscriptions: {[id: string]: IWidgetSubscription} = {};
defaultSubscription: IWidgetSubscription = null; defaultSubscription: IWidgetSubscription = null;
labelPatterns: {[id: string]: LabelVariablePattern} = {};
timewindowFunctions: TimewindowFunctions = { timewindowFunctions: TimewindowFunctions = {
onUpdateTimewindow: (startTimeMs, endTimeMs, interval) => { onUpdateTimewindow: (startTimeMs, endTimeMs, interval) => {
if (this.defaultSubscription) { if (this.defaultSubscription) {
@ -265,7 +269,7 @@ export class WidgetContext {
hiddenData?: Array<{data: DataSet}>; hiddenData?: Array<{data: DataSet}>;
timeWindow?: WidgetTimewindow; timeWindow?: WidgetTimewindow;
absoluteHeader?: boolean; embedTitlePanel?: boolean;
hideTitlePanel = false; hideTitlePanel = false;
@ -312,6 +316,23 @@ export class WidgetContext {
}); });
} }
registerLabelPattern(id: string, label: string): Observable<string> {
let labelPattern = this.labelPatterns[id];
if (labelPattern) {
labelPattern.setupPattern(label);
} else {
labelPattern = new LabelVariablePattern(label, this);
this.labelPatterns[id] = labelPattern;
}
return labelPattern.label$;
}
updateLabelPatterns() {
for (const key of Object.keys(this.labelPatterns)) {
this.labelPatterns[key].update();
}
}
showSuccessToast(message: string, duration: number = 1000, showSuccessToast(message: string, duration: number = 1000,
verticalPosition: NotificationVerticalPosition = 'bottom', verticalPosition: NotificationVerticalPosition = 'bottom',
horizontalPosition: NotificationHorizontalPosition = 'left', horizontalPosition: NotificationHorizontalPosition = 'left',
@ -406,6 +427,14 @@ export class WidgetContext {
this.widgetActions = undefined; this.widgetActions = undefined;
} }
destroy() {
for (const key of Object.keys(this.labelPatterns)) {
this.labelPatterns[key].destroy();
}
this.labelPatterns = {};
this.destroyed = true;
}
closeDialog(resultData: any = null) { closeDialog(resultData: any = null) {
const dialogRef = this.$scope.dialogRef || this.stateController.dashboardCtrl.dashboardCtx.getDashboard().dialogRef; const dialogRef = this.$scope.dialogRef || this.stateController.dashboardCtrl.dashboardCtx.getDashboard().dialogRef;
if (dialogRef) { if (dialogRef) {
@ -426,6 +455,41 @@ 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;
if (this.hasVariables && this.ctx.defaultSubscription?.datasources?.length) {
label = createLabelFromDatasource(this.ctx.defaultSubscription.datasources[0], label);
}
if (this.labelSubject.value !== label) {
this.labelSubject.next(label);
}
}
destroy() {
this.labelSubject.complete();
}
}
export interface IDynamicWidgetComponent { export interface IDynamicWidgetComponent {
readonly ctx: WidgetContext; readonly ctx: WidgetContext;
readonly errorMessages: string[]; readonly errorMessages: string[];
@ -446,7 +510,8 @@ export interface WidgetInfo extends WidgetTypeDescriptor, WidgetControllerDescri
typeLatestDataKeySettingsSchema?: string | any; typeLatestDataKeySettingsSchema?: string | any;
image?: string; image?: string;
description?: string; description?: string;
componentFactory?: ComponentFactory<IDynamicWidgetComponent>; componentType?: Type<IDynamicWidgetComponent>;
componentModuleRef?: NgModuleRef<DynamicComponentModule>;
} }
export interface WidgetConfigComponentData { 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(); $event.stopPropagation();
this.dialogs.colorPicker(this.colorFormGroup.get('color').value, this.dialogs.colorPicker(this.colorFormGroup.get('color').value,
this.colorClearButton).subscribe( this.colorClearButton).subscribe(
(color) => { (result) => {
if (color) { if (!result?.canceled) {
this.colorFormGroup.patchValue( this.colorFormGroup.patchValue(
{color}, {emitEvent: true} {color: result?.color}, {emitEvent: true}
); );
this.cd.markForCheck(); 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; colorClearButton: boolean;
} }
export interface ColorPickerDialogResult {
color?: string;
canceled?: boolean;
}
@Component({ @Component({
selector: 'tb-color-picker-dialog', selector: 'tb-color-picker-dialog',
templateUrl: './color-picker-dialog.component.html', templateUrl: './color-picker-dialog.component.html',
styleUrls: ['./color-picker-dialog.component.scss'] styleUrls: ['./color-picker-dialog.component.scss']
}) })
export class ColorPickerDialogComponent extends DialogComponent<ColorPickerDialogComponent, string> { export class ColorPickerDialogComponent extends DialogComponent<ColorPickerDialogComponent, ColorPickerDialogResult> {
color: string; color: string;
colorClearButton: boolean; colorClearButton: boolean;
@ -39,18 +44,18 @@ export class ColorPickerDialogComponent extends DialogComponent<ColorPickerDialo
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
protected router: Router, protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: ColorPickerDialogData, @Inject(MAT_DIALOG_DATA) public data: ColorPickerDialogData,
public dialogRef: MatDialogRef<ColorPickerDialogComponent, string>) { public dialogRef: MatDialogRef<ColorPickerDialogComponent, ColorPickerDialogResult>) {
super(store, router, dialogRef); super(store, router, dialogRef);
this.color = data.color; this.color = data.color;
this.colorClearButton = data.colorClearButton; this.colorClearButton = data.colorClearButton;
} }
selectColor(color: string) { selectColor(color: string) {
this.dialogRef.close(color); this.dialogRef.close({color});
} }
cancel(): void { 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> <mat-icon>close</mat-icon>
</button> </button>
<tb-material-icons [selectedIcon]="selectedIcon" <tb-material-icons [selectedIcon]="selectedIcon"
[iconClearButton]="iconClearButton"
(iconSelected)="selectIcon($event)"> (iconSelected)="selectIcon($event)">
</tb-material-icons> </tb-material-icons>
</div> </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 { export interface MaterialIconsDialogData {
icon: string; icon: string;
iconClearButton: boolean;
}
export interface MaterialIconsDialogResult {
icon?: string;
canceled?: boolean;
} }
@Component({ @Component({
@ -31,24 +37,26 @@ export interface MaterialIconsDialogData {
providers: [], providers: [],
styleUrls: ['./material-icons-dialog.component.scss'] styleUrls: ['./material-icons-dialog.component.scss']
}) })
export class MaterialIconsDialogComponent extends DialogComponent<MaterialIconsDialogComponent, string> { export class MaterialIconsDialogComponent extends DialogComponent<MaterialIconsDialogComponent, MaterialIconsDialogResult> {
selectedIcon: string; selectedIcon: string;
iconClearButton: boolean;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
protected router: Router, protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: MaterialIconsDialogData, @Inject(MAT_DIALOG_DATA) public data: MaterialIconsDialogData,
public dialogRef: MatDialogRef<MaterialIconsDialogComponent, string>) { public dialogRef: MatDialogRef<MaterialIconsDialogComponent, MaterialIconsDialogResult>) {
super(store, router, dialogRef); super(store, router, dialogRef);
this.selectedIcon = data.icon; this.selectedIcon = data.icon;
this.iconClearButton = data.iconClearButton;
} }
selectIcon(icon: string) { selectIcon(icon: string) {
this.dialogRef.close(icon); this.dialogRef.close({icon});
} }
cancel(): void { 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) this._contentChanges = this.contentObserver.observe(this._iconNameContent.nativeElement)
.subscribe(() => { .subscribe(() => {
const content = this.viewValue; const content = this.viewValue;
if (content && this.icon !== content) { if (this.icon !== content) {
this.icon = content; this.icon = content;
this._updateIcon(); 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)[], private onColorClick(key: (string | number)[],
val: tinycolor.ColorFormats.RGBA, val: tinycolor.ColorFormats.RGBA,
colorSelectedFn: (color: tinycolor.ColorFormats.RGBA) => void) { colorSelectedFn: (color: tinycolor.ColorFormats.RGBA) => void) {
this.dialogs.colorPicker(tinycolor(val).toRgbString()).subscribe((color) => { this.dialogs.colorPicker(tinycolor(val).toRgbString()).subscribe((result) => {
if (color && colorSelectedFn) { if (!result?.canceled && colorSelectedFn) {
colorSelectedFn(tinycolor(color).toRgb()); colorSelectedFn(tinycolor(result?.color).toRgb());
} }
}); });
} }
@ -226,9 +226,9 @@ export class JsonFormComponent implements OnInit, ControlValueAccessor, Validato
private onIconClick(key: (string | number)[], private onIconClick(key: (string | number)[],
val: string, val: string,
iconSelectedFn: (icon: string) => void) { iconSelectedFn: (icon: string) => void) {
this.dialogs.materialIconPicker(val).subscribe((icon) => { this.dialogs.materialIconPicker(val).subscribe((result) => {
if (icon && iconSelectedFn) { if (!result?.canceled && iconSelectedFn) {
iconSelectedFn(icon); iconSelectedFn(result?.icon);
} }
}); });
} }

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

@ -17,7 +17,6 @@
import { import {
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
ComponentFactory,
ComponentRef, ComponentRef,
ElementRef, ElementRef,
EventEmitter, EventEmitter,
@ -91,7 +90,7 @@ export class TbMarkdownComponent implements OnChanges {
error = null; error = null;
private tbMarkdownInstanceComponentRef: ComponentRef<any>; private tbMarkdownInstanceComponentRef: ComponentRef<any>;
private tbMarkdownInstanceComponentFactory: ComponentFactory<any>; private tbMarkdownInstanceComponentType: Type<any>;
constructor(private help: HelpService, constructor(private help: HelpService,
private cd: ChangeDetectorRef, private cd: ChangeDetectorRef,
@ -153,7 +152,7 @@ export class TbMarkdownComponent implements OnChanges {
if (this.additionalCompileModules) { if (this.additionalCompileModules) {
compileModules = compileModules.concat(this.additionalCompileModules); compileModules = compileModules.concat(this.additionalCompileModules);
} }
this.dynamicComponentFactoryService.createDynamicComponentFactory( this.dynamicComponentFactoryService.createDynamicComponent(
class TbMarkdownInstance { class TbMarkdownInstance {
ngOnDestroy(): void { ngOnDestroy(): void {
parent.destroyMarkdownInstanceResources(); parent.destroyMarkdownInstanceResources();
@ -162,12 +161,13 @@ export class TbMarkdownComponent implements OnChanges {
template, template,
compileModules, compileModules,
true, 1, styles true, 1, styles
).subscribe((factory) => { ).subscribe((componentData) => {
this.tbMarkdownInstanceComponentFactory = factory; this.tbMarkdownInstanceComponentType = componentData.componentType;
const injector: Injector = Injector.create({providers: [], parent: this.markdownContainer.injector}); const injector: Injector = Injector.create({providers: [], parent: this.markdownContainer.injector});
try { try {
this.tbMarkdownInstanceComponentRef = this.tbMarkdownInstanceComponentRef =
this.markdownContainer.createComponent(this.tbMarkdownInstanceComponentFactory, 0, injector); this.markdownContainer.createComponent(this.tbMarkdownInstanceComponentType,
{index: 0, injector, ngModuleRef: componentData.componentModuleRef});
if (this.context) { if (this.context) {
for (const propName of Object.keys(this.context)) { for (const propName of Object.keys(this.context)) {
this.tbMarkdownInstanceComponentRef.instance[propName] = this.context[propName]; this.tbMarkdownInstanceComponentRef.instance[propName] = this.context[propName];
@ -261,9 +261,9 @@ export class TbMarkdownComponent implements OnChanges {
} }
private destroyMarkdownInstanceResources() { private destroyMarkdownInstanceResources() {
if (this.tbMarkdownInstanceComponentFactory) { if (this.tbMarkdownInstanceComponentType) {
this.dynamicComponentFactoryService.destroyDynamicComponentFactory(this.tbMarkdownInstanceComponentFactory); this.dynamicComponentFactoryService.destroyDynamicComponent(this.tbMarkdownInstanceComponentType);
this.tbMarkdownInstanceComponentFactory = null; this.tbMarkdownInstanceComponentType = null;
} }
this.tbMarkdownInstanceComponentRef = 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() @Input()
disabled: boolean; disabled: boolean;
private iconClearButtonValue: boolean;
get iconClearButton(): boolean {
return this.iconClearButtonValue;
}
@Input() @Input()
set iconClearButton(value: boolean) { @coerceBoolean()
const newVal = coerceBooleanProperty(value); iconClearButton = false;
if (this.iconClearButtonValue !== newVal) {
this.iconClearButtonValue = newVal;
}
}
private requiredValue: boolean; private requiredValue: boolean;
get required(): boolean { get required(): boolean {
@ -135,11 +127,12 @@ export class MaterialIconSelectComponent extends PageComponent implements OnInit
openIconDialog() { openIconDialog() {
if (!this.disabled) { if (!this.disabled) {
this.dialogs.materialIconPicker(this.materialIconFormGroup.get('icon').value).subscribe( this.dialogs.materialIconPicker(this.materialIconFormGroup.get('icon').value,
(icon) => { this.iconClearButton).subscribe(
if (icon) { (result) => {
if (!result?.canceled) {
this.materialIconFormGroup.patchValue( this.materialIconFormGroup.patchValue(
{icon}, {emitEvent: true} {icon: result?.icon}, {emitEvent: true}
); );
this.cd.markForCheck(); this.cd.markForCheck();
} }
@ -159,7 +152,8 @@ export class MaterialIconSelectComponent extends PageComponent implements OnInit
const materialIconsPopover = this.popoverService.displayPopover(trigger, this.renderer, const materialIconsPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, MaterialIconsComponent, 'left', true, null, this.viewContainerRef, MaterialIconsComponent, 'left', true, null,
{ {
selectedIcon: this.materialIconFormGroup.get('icon').value selectedIcon: this.materialIconFormGroup.get('icon').value,
iconClearButton: this.iconClearButton
}, },
{}, {},
{}, {}, true); {}, {}, 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 class="tb-no-data-text">{{ 'icon.no-icons-found' | translate:{iconSearch: searchIconControl.value} }}</div>
</div> </div>
</ng-container> </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> </div>

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

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

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

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

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

@ -24,15 +24,24 @@
max-width: 100%; max-width: 100%;
} }
section.tb-timewindow { section.tb-timewindow {
min-height: 32px;
padding: 0 8px; 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; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
pointer-events: all;
cursor: pointer;
} }
.timezone-abbr { .timezone-abbr {

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

@ -20,7 +20,7 @@ import {
ElementRef, ElementRef,
forwardRef, HostBinding, forwardRef, HostBinding,
Injector, Injector,
Input, Input, OnChanges, OnInit, SimpleChanges,
StaticProvider, StaticProvider,
ViewContainerRef ViewContainerRef
} from '@angular/core'; } from '@angular/core';
@ -50,6 +50,12 @@ import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
import { ComponentPortal } from '@angular/cdk/portal'; import { ComponentPortal } from '@angular/cdk/portal';
import { coerceBoolean } from '@shared/decorators/coercion'; import { coerceBoolean } from '@shared/decorators/coercion';
import {
ComponentStyle,
defaultTimewindowStyle, iconStyle,
textStyle,
TimewindowStyle
} from '@shared/models/widget-settings.models';
// @dynamic // @dynamic
@Component({ @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; historyOnlyValue = false;
@ -88,6 +94,14 @@ export class TimewindowComponent implements ControlValueAccessor {
@coerceBoolean() @coerceBoolean()
noMargin = false; noMargin = false;
@Input()
@coerceBoolean()
noPadding = false;
@Input()
@coerceBoolean()
disablePanel = false;
@Input() @Input()
@coerceBoolean() @coerceBoolean()
forAllTimeEnabled = false; forAllTimeEnabled = false;
@ -145,10 +159,10 @@ export class TimewindowComponent implements ControlValueAccessor {
} }
@Input() @Input()
direction: 'left' | 'right' = 'left'; tooltipPosition: TooltipPosition = 'above';
@Input() @Input()
tooltipPosition: TooltipPosition = 'above'; timewindowStyle: TimewindowStyle;
@Input() @Input()
@coerceBoolean() @coerceBoolean()
@ -158,6 +172,10 @@ export class TimewindowComponent implements ControlValueAccessor {
timewindowDisabled: boolean; timewindowDisabled: boolean;
computedTimewindowStyle: TimewindowStyle;
timewindowComponentStyle: ComponentStyle;
timewindowIconStyle: ComponentStyle;
private propagateChange = (_: any) => {}; private propagateChange = (_: any) => {};
constructor(private overlay: Overlay, constructor(private overlay: Overlay,
@ -170,10 +188,28 @@ export class TimewindowComponent implements ControlValueAccessor {
public viewContainerRef: ViewContainerRef) { 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) { toggleTimewindow($event: Event) {
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
} }
if (this.disablePanel) {
return;
}
const config = new OverlayConfig({ const config = new OverlayConfig({
panelClass: 'tb-timewindow-panel', panelClass: 'tb-timewindow-panel',
backdropClass: 'cdk-overlay-transparent-backdrop', backdropClass: 'cdk-overlay-transparent-backdrop',
@ -226,6 +262,17 @@ export class TimewindowComponent implements ControlValueAccessor {
this.cd.detectChanges(); 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 { private onHistoryOnlyChanged(): boolean {
if (this.historyOnlyValue && this.innerValue && this.innerValue.selectedTab !== TimewindowType.HISTORY) { if (this.historyOnlyValue && this.innerValue && this.innerValue.selectedTab !== TimewindowType.HISTORY) {
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; family: string;
weight: fontWeight; weight: fontWeight;
style: fontStyle; style: fontStyle;
lineHeight: string;
} }
export enum ColorType { export enum ColorType {
@ -89,6 +90,22 @@ export interface ColorSettings {
colorFunction?: string; 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 => ({ export const constantColor = (color: string): ColorSettings => ({
type: ColorType.constant, type: ColorType.constant,
color, color,
@ -298,19 +315,19 @@ export interface BackgroundSettings {
overlay: OverlaySettings; overlay: OverlaySettings;
} }
export const iconStyle = (size: number, sizeUnit: cssUnit): ComponentStyle => { export const iconStyle = (size: number | string, sizeUnit: cssUnit = 'px'): ComponentStyle => {
const iconSize = size + sizeUnit; const iconSize = typeof size === 'number' ? size + sizeUnit : size;
return { return {
width: iconSize, width: iconSize,
minWidth: iconSize,
height: iconSize, height: iconSize,
fontSize: iconSize, fontSize: iconSize,
lineHeight: 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 = { const style: ComponentStyle = {
lineHeight,
letterSpacing letterSpacing
}; };
if (font?.style) { if (font?.style) {
@ -319,6 +336,9 @@ export const textStyle = (font?: Font, lineHeight = '1.5', letterSpacing = '0.25
if (font?.weight) { if (font?.weight) {
style.fontWeight = font.weight; style.fontWeight = font.weight;
} }
if (font?.lineHeight) {
style.lineHeight = font.lineHeight;
}
if (font?.size) { if (font?.size) {
style.fontSize = (font.size + (font.sizeUnit || 'px')); 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 { IAliasController } from '@core/api/widget-api.models';
import { isEmptyStr } from '@core/utils'; import { isEmptyStr } from '@core/utils';
import { WidgetConfigComponentData } from '@home/models/widget-component.models'; 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 { export enum widgetType {
timeseries = 'timeseries', timeseries = 'timeseries',
@ -183,7 +183,7 @@ export interface WidgetTypeParameters {
processNoDataByWidget?: boolean; processNoDataByWidget?: boolean;
previewWidth?: string; previewWidth?: string;
previewHeight?: string; previewHeight?: string;
absoluteHeader?: boolean; embedTitlePanel?: boolean;
} }
export interface WidgetControllerDescriptor { export interface WidgetControllerDescriptor {
@ -633,6 +633,7 @@ export interface WidgetConfig {
useDashboardTimewindow?: boolean; useDashboardTimewindow?: boolean;
displayTimewindow?: boolean; displayTimewindow?: boolean;
timewindow?: Timewindow; timewindow?: Timewindow;
timewindowStyle?: TimewindowStyle;
desktopHide?: boolean; desktopHide?: boolean;
mobileHide?: boolean; mobileHide?: boolean;
mobileHeight?: number; mobileHeight?: number;

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

@ -3902,7 +3902,15 @@
"interval": "Interval", "interval": "Interval",
"just-now": "Just now", "just-now": "Just now",
"just-now-lower": "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": { "unit": {
"millimeter": "Millimeter", "millimeter": "Millimeter",
@ -5799,7 +5807,8 @@
"font-weight-lighter": "Lighter", "font-weight-lighter": "Lighter",
"color": "Color", "color": "Color",
"shadow-color": "Shadow color", "shadow-color": "Shadow color",
"preview": "Preview" "preview": "Preview",
"line-height": "Line height"
}, },
"home": { "home": {
"no-data-available": "No data available" "no-data-available": "No data available"

Loading…
Cancel
Save