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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -0,0 +1,97 @@
<!--
Copyright © 2016-2023 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="tb-timewindow-style-panel" [formGroup]="timewindowStyleFormGroup">
<div class="tb-timewindow-style-title" translate>timewindow.style</div>
<div class="tb-form-row space-between">
<mat-slide-toggle class="mat-slide" formControlName="showIcon">
{{ 'timewindow.icon' | translate }}
</mat-slide-toggle>
<div fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="iconSize">
</mat-form-field>
<tb-material-icon-select asBoxInput
iconClearButton
formControlName="icon">
</tb-material-icon-select>
</div>
</div>
<div class="tb-form-row">
<div class="fixed-title-width" translate>timewindow.icon-position</div>
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="iconPosition" placeholder="{{ 'widget-config.set' | translate }}">
<mat-option [value]="'left'">
{{ 'timewindow.icon-position-left' | translate }}
</mat-option>
<mat-option [value]="'right'">
{{ 'timewindow.icon-position-right' | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="tb-form-row space-between">
<div translate>timewindow.font</div>
<tb-font-settings
clearButton
[previewText]="timewindowComponent.displayValue()"
[initialPreviewStyle]="{fontSize: '14px',
color: 'rgba(0, 0, 0, 0.87)',
opacity: '0.85',
lineHeight: 'normal',
letterSpacing: 'normal'}"
formControlName="font">
</tb-font-settings>
</div>
<div class="tb-form-row space-between">
<div translate>timewindow.color</div>
<tb-color-input
formControlName="color"
colorClearButton
asBoxInput>
</tb-color-input>
</div>
<mat-divider></mat-divider>
<div class="tb-form-row no-border no-padding timewindow-preview">
<div class="fixed-title-width" translate>timewindow.preview</div>
<tb-timewindow #timewindowComponent
disablePanel
alwaysDisplayTypePrefix
timezone="true"
noMargin
noPadding
[timewindowStyle]="previewTimewindowStyle"
[ngModel]="previewValue"
[ngModelOptions]="{standalone: true}">
</tb-timewindow>
</div>
<div class="tb-timewindow-style-panel-buttons">
<button mat-button
color="primary"
type="button"
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button
color="primary"
type="button"
(click)="applyTimewindowStyle()"
[disabled]="timewindowStyleFormGroup.invalid || !timewindowStyleFormGroup.dirty">
{{ 'action.apply' | translate }}
</button>
</div>
</div>

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

@ -0,0 +1,54 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@import '../../../../../../scss/constants';
.tb-timewindow-style-panel {
width: 100%;
display: flex;
flex-direction: column;
gap: 16px;
@media #{$mat-gt-xs} {
min-width: 320px;
}
.tb-timewindow-style-title {
font-size: 16px;
font-weight: 500;
line-height: 24px;
letter-spacing: 0.25px;
color: rgba(0, 0, 0, 0.87);
}
.tb-form-row {
.fixed-title-width {
min-width: 120px;
}
&.timewindow-preview {
align-items: flex-start;
tb-timewindow {
font-size: 14px;
opacity: .85;
}
}
}
.tb-timewindow-style-panel-buttons {
height: 60px;
display: flex;
flex-direction: row;
gap: 16px;
justify-content: flex-end;
align-items: flex-end;
}
}

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

@ -0,0 +1,108 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { defaultTimewindowStyle, TimewindowStyle } from '@shared/models/widget-settings.models';
import { TbPopoverComponent } from '@shared/components/popover.component';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Timewindow } from '@shared/models/time/time.models';
import { deepClone } from '@core/utils';
@Component({
selector: 'tb-timewindow-style-panel',
templateUrl: './timewindow-style-panel.component.html',
providers: [],
styleUrls: ['./timewindow-style-panel.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class TimewindowStylePanelComponent extends PageComponent implements OnInit {
@Input()
timewindowStyle: TimewindowStyle;
@Input()
previewValue: Timewindow;
@Input()
popover: TbPopoverComponent<TimewindowStylePanelComponent>;
@Output()
timewindowStyleApplied = new EventEmitter<TimewindowStyle>();
timewindowStyleFormGroup: UntypedFormGroup;
previewTimewindowStyle: TimewindowStyle;
constructor(private fb: UntypedFormBuilder,
protected store: Store<AppState>) {
super(store);
}
ngOnInit(): void {
const computedTimewindowStyle = {...defaultTimewindowStyle, ...(this.timewindowStyle || {})};
this.timewindowStyleFormGroup = this.fb.group(
{
showIcon: [computedTimewindowStyle.showIcon, []],
iconSize: [computedTimewindowStyle.iconSize, []],
icon: [computedTimewindowStyle.icon, []],
iconPosition: [computedTimewindowStyle.iconPosition, []],
font: [computedTimewindowStyle.font, []],
color: [computedTimewindowStyle.color, []]
}
);
this.updatePreviewStyle(this.timewindowStyle);
this.updateTimewindowStyleEnabledState();
this.timewindowStyleFormGroup.valueChanges.subscribe((timewindowStyle: TimewindowStyle) => {
if (this.timewindowStyleFormGroup.valid) {
this.updatePreviewStyle(timewindowStyle);
setTimeout(() => {this.popover?.updatePosition();}, 0);
}
});
this.timewindowStyleFormGroup.get('showIcon').valueChanges.subscribe(() => {
this.updateTimewindowStyleEnabledState();
});
}
cancel() {
this.popover?.hide();
}
applyTimewindowStyle() {
const timewindowStyle = this.timewindowStyleFormGroup.getRawValue();
this.timewindowStyleApplied.emit(timewindowStyle);
}
private updateTimewindowStyleEnabledState() {
const showIcon: boolean = this.timewindowStyleFormGroup.get('showIcon').value;
if (showIcon) {
this.timewindowStyleFormGroup.get('iconSize').enable({emitEvent: false});
this.timewindowStyleFormGroup.get('icon').enable({emitEvent: false});
this.timewindowStyleFormGroup.get('iconPosition').enable({emitEvent: false});
} else {
this.timewindowStyleFormGroup.get('iconSize').disable({emitEvent: false});
this.timewindowStyleFormGroup.get('icon').disable({emitEvent: false});
this.timewindowStyleFormGroup.get('iconPosition').disable({emitEvent: false});
}
}
private updatePreviewStyle(timewindowStyle: TimewindowStyle) {
this.previewTimewindowStyle = deepClone(timewindowStyle);
}
}

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

@ -0,0 +1,25 @@
<!--
Copyright © 2016-2023 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<button type="button"
mat-stroked-button
class="tb-box-button"
[disabled]="disabled"
#matButton
(click)="openTimewindowStylePopup($event, matButton)">
<tb-icon matButtonIcon>format_paint</tb-icon>
</button>

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

@ -0,0 +1,97 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, forwardRef, Input, OnInit, Renderer2, ViewContainerRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { TimewindowStyle } from '@shared/models/widget-settings.models';
import { MatButton } from '@angular/material/button';
import { TbPopoverService } from '@shared/components/popover.service';
import { Timewindow } from '@shared/models/time/time.models';
import { TimewindowStylePanelComponent } from '@home/components/widget/config/timewindow-style-panel.component';
@Component({
selector: 'tb-timewindow-style',
templateUrl: './timewindow-style.component.html',
styleUrls: [],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => TimewindowStyleComponent),
multi: true
}
]
})
export class TimewindowStyleComponent implements OnInit, ControlValueAccessor {
@Input()
disabled: boolean;
@Input()
previewValue: Timewindow;
private modelValue: TimewindowStyle;
private propagateChange = null;
constructor(private popoverService: TbPopoverService,
private renderer: Renderer2,
private viewContainerRef: ViewContainerRef) {}
ngOnInit(): void {
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}
writeValue(value: TimewindowStyle): void {
this.modelValue = value;
}
openTimewindowStylePopup($event: Event, matButton: MatButton) {
if ($event) {
$event.stopPropagation();
}
const trigger = matButton._elementRef.nativeElement;
if (this.popoverService.hasPopover(trigger)) {
this.popoverService.hidePopover(trigger);
} else {
const ctx: any = {
timewindowStyle: this.modelValue,
previewValue: this.previewValue
};
const timewindowStylePanelPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, TimewindowStylePanelComponent, 'left', true, null,
ctx,
{},
{}, {}, true);
timewindowStylePanelPopover.tbComponentRef.instance.popover = timewindowStylePanelPopover;
timewindowStylePanelPopover.tbComponentRef.instance.timewindowStyleApplied.subscribe((timewindowStyle) => {
timewindowStylePanelPopover.hide();
this.modelValue = timewindowStyle;
this.propagateChange(this.modelValue);
});
}
}
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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>
<tb-css-unit-select fxFlex formControlName="iconSizeUnit"></tb-css-unit-select>
<tb-material-icon-select asBoxInput
iconClearButton
[color]="valueCardWidgetSettingsForm.get('iconColor').value?.color"
formControlName="icon">
</tb-material-icon-select>

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -45,13 +45,13 @@ import {
WidgetActionsApi,
WidgetSubscriptionApi
} from '@core/api/widget-api.models';
import { ChangeDetectorRef, ComponentFactory, Injector, NgZone, Type } from '@angular/core';
import { ChangeDetectorRef, Injector, NgModuleRef, NgZone, Type } from '@angular/core';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { RafService } from '@core/services/raf.service';
import { WidgetTypeId } from '@shared/models/id/widget-type-id';
import { TenantId } from '@shared/models/id/tenant-id';
import { WidgetLayout } from '@shared/models/dashboard.models';
import { formatValue, isDefined } from '@core/utils';
import { createLabelFromDatasource, formatValue, hasDatasourceLabelsVariables, isDefined } from '@core/utils';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import {
@ -87,10 +87,12 @@ import * as RxJS from 'rxjs';
import * as RxJSOperators from 'rxjs/operators';
import { TbPopoverComponent } from '@shared/components/popover.component';
import { EntityId } from '@shared/models/id/entity-id';
import { AlarmQuery, AlarmSearchStatus, AlarmStatus} from '@app/shared/models/alarm.models';
import { AlarmQuery, AlarmSearchStatus, AlarmStatus } from '@app/shared/models/alarm.models';
import { MillisecondsToTimeStringPipe, TelemetrySubscriber } from '@app/shared/public-api';
import { UserId } from '@shared/models/id/user-id';
import { UserSettingsService } from '@core/http/user-settings.service';
import { DynamicComponentModule } from '@core/services/dynamic-component-factory.service';
import { BehaviorSubject, Observable } from 'rxjs';
export interface IWidgetAction {
name: string;
@ -200,6 +202,8 @@ export class WidgetContext {
subscriptions: {[id: string]: IWidgetSubscription} = {};
defaultSubscription: IWidgetSubscription = null;
labelPatterns: {[id: string]: LabelVariablePattern} = {};
timewindowFunctions: TimewindowFunctions = {
onUpdateTimewindow: (startTimeMs, endTimeMs, interval) => {
if (this.defaultSubscription) {
@ -265,7 +269,7 @@ export class WidgetContext {
hiddenData?: Array<{data: DataSet}>;
timeWindow?: WidgetTimewindow;
absoluteHeader?: boolean;
embedTitlePanel?: boolean;
hideTitlePanel = false;
@ -312,6 +316,23 @@ export class WidgetContext {
});
}
registerLabelPattern(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,
verticalPosition: NotificationVerticalPosition = 'bottom',
horizontalPosition: NotificationHorizontalPosition = 'left',
@ -406,6 +427,14 @@ export class WidgetContext {
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) {
const dialogRef = this.$scope.dialogRef || this.stateController.dashboardCtrl.dashboardCtx.getDashboard().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 {
readonly ctx: WidgetContext;
readonly errorMessages: string[];
@ -446,7 +510,8 @@ export interface WidgetInfo extends WidgetTypeDescriptor, WidgetControllerDescri
typeLatestDataKeySettingsSchema?: string | any;
image?: string;
description?: string;
componentFactory?: ComponentFactory<IDynamicWidgetComponent>;
componentType?: Type<IDynamicWidgetComponent>;
componentModuleRef?: NgModuleRef<DynamicComponentModule>;
}
export interface WidgetConfigComponentData {

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -3902,7 +3902,15 @@
"interval": "Interval",
"just-now": "Just now",
"just-now-lower": "just now",
"ago": "ago"
"ago": "ago",
"style": "Timewindow style",
"icon": "Icon",
"icon-position": "Icon position",
"icon-position-left": "Left",
"icon-position-right": "Right",
"font": "Font",
"color": "Color",
"preview": "Preview"
},
"unit": {
"millimeter": "Millimeter",
@ -5799,7 +5807,8 @@
"font-weight-lighter": "Lighter",
"color": "Color",
"shadow-color": "Shadow color",
"preview": "Preview"
"preview": "Preview",
"line-height": "Line height"
},
"home": {
"no-data-available": "No data available"

Loading…
Cancel
Save