UI: add popover component to wiget context

This commit is contained in:
Igor Kulikov 2022-10-07 19:33:18 +03:00
parent 990a50391e
commit 65438c64e7
10 changed files with 116 additions and 32 deletions

View File

@ -185,7 +185,8 @@
[isEditingWidget]="isEditingWidget" [isEditingWidget]="isEditingWidget"
[isMobile]="forceDashboardMobileMode" [isMobile]="forceDashboardMobileMode"
[widgetEditMode]="widgetEditMode" [widgetEditMode]="widgetEditMode"
[parentDashboard]="parentDashboard"> [parentDashboard]="parentDashboard"
[popoverComponent]="popoverComponent">
</tb-dashboard-layout> </tb-dashboard-layout>
</mat-drawer> </mat-drawer>
<mat-drawer-content [fxShow]="layouts.main.show" <mat-drawer-content [fxShow]="layouts.main.show"
@ -200,7 +201,8 @@
[isEditingWidget]="isEditingWidget" [isEditingWidget]="isEditingWidget"
[isMobile]="forceDashboardMobileMode" [isMobile]="forceDashboardMobileMode"
[widgetEditMode]="widgetEditMode" [widgetEditMode]="widgetEditMode"
[parentDashboard]="parentDashboard"> [parentDashboard]="parentDashboard"
[popoverComponent]="popoverComponent">
</tb-dashboard-layout> </tb-dashboard-layout>
</mat-drawer-content> </mat-drawer-content>
</mat-drawer-container> </mat-drawer-container>

View File

@ -17,13 +17,18 @@
import { import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
ChangeDetectorRef, ChangeDetectorRef,
Component, ElementRef, EventEmitter, HostBinding, Component,
ElementRef,
EventEmitter,
HostBinding,
Inject, Inject,
Injector, Injector,
Input, Input,
NgZone, NgZone,
OnDestroy, OnDestroy,
OnInit, Optional, Renderer2, OnInit,
Optional,
Renderer2,
StaticProvider, StaticProvider,
ViewChild, ViewChild,
ViewContainerRef, ViewContainerRef,
@ -48,7 +53,7 @@ import {
} from '@app/shared/models/dashboard.models'; } from '@app/shared/models/dashboard.models';
import { WINDOW } from '@core/services/window.service'; import { WINDOW } from '@core/services/window.service';
import { WindowMessage } from '@shared/models/window-message.model'; import { WindowMessage } from '@shared/models/window-message.model';
import { deepClone, guid, hashCode, isDefined, isDefinedAndNotNull, isNotEmptyStr } from '@app/core/utils'; import { deepClone, guid, isDefined, isDefinedAndNotNull, isNotEmptyStr } from '@app/core/utils';
import { import {
DashboardContext, DashboardContext,
DashboardPageLayout, DashboardPageLayout,
@ -129,7 +134,8 @@ import { MobileService } from '@core/services/mobile.service';
import { import {
DashboardImageDialogComponent, DashboardImageDialogComponent,
DashboardImageDialogData, DashboardImageDialogResult DashboardImageDialogData,
DashboardImageDialogResult
} from '@home/components/dashboard-page/dashboard-image-dialog.component'; } from '@home/components/dashboard-page/dashboard-image-dialog.component';
import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
import cssjs from '@core/css/css'; import cssjs from '@core/css/css';
@ -139,6 +145,7 @@ import { MatButton } from '@angular/material/button';
import { VersionControlComponent } from '@home/components/vc/version-control.component'; import { VersionControlComponent } from '@home/components/vc/version-control.component';
import { TbPopoverService } from '@shared/components/popover.service'; import { TbPopoverService } from '@shared/components/popover.service';
import { tap } from 'rxjs/operators'; import { tap } from 'rxjs/operators';
import { TbPopoverComponent } from '@shared/components/popover.component';
// @dynamic // @dynamic
@Component({ @Component({
@ -184,6 +191,9 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
@Input() @Input()
parentDashboard?: IDashboardComponent = null; parentDashboard?: IDashboardComponent = null;
@Input()
popoverComponent?: TbPopoverComponent = null;
@Input() @Input()
parentAliasController?: IAliasController = null; parentAliasController?: IAliasController = null;

View File

@ -58,6 +58,7 @@
[isRemoveActionEnabled]="isEdit && !widgetEditMode" [isRemoveActionEnabled]="isEdit && !widgetEditMode"
[callbacks]="this" [callbacks]="this"
[ignoreLoading]="layoutCtx.ignoreLoading" [ignoreLoading]="layoutCtx.ignoreLoading"
[parentDashboard]="parentDashboard"> [parentDashboard]="parentDashboard"
[popoverComponent]="popoverComponent">
</tb-dashboard> </tb-dashboard>
</div> </div>

View File

@ -33,6 +33,7 @@ import { TranslateService } from '@ngx-translate/core';
import { ItemBufferService } from '@app/core/services/item-buffer.service'; import { ItemBufferService } from '@app/core/services/item-buffer.service';
import { DomSanitizer, SafeStyle } from '@angular/platform-browser'; import { DomSanitizer, SafeStyle } from '@angular/platform-browser';
import { TbCheatSheetComponent } from '@shared/components/cheatsheet.component'; import { TbCheatSheetComponent } from '@shared/components/cheatsheet.component';
import { TbPopoverComponent } from '@shared/components/popover.component';
@Component({ @Component({
selector: 'tb-dashboard-layout', selector: 'tb-dashboard-layout',
@ -81,6 +82,9 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo
@Input() @Input()
parentDashboard?: IDashboardComponent = null; parentDashboard?: IDashboardComponent = null;
@Input()
popoverComponent?: TbPopoverComponent = null;
@ViewChild('dashboard', {static: true}) dashboard: IDashboardComponent; @ViewChild('dashboard', {static: true}) dashboard: IDashboardComponent;
private rxSubscriptions = new Array<Subscription>(); private rxSubscriptions = new Array<Subscription>();

View File

@ -15,7 +15,9 @@
/// ///
import { import {
AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component, Component,
DoCheck, DoCheck,
Input, Input,
@ -56,6 +58,7 @@ import { distinct } from 'rxjs/operators';
import { ResizeObserver } from '@juggle/resize-observer'; import { ResizeObserver } from '@juggle/resize-observer';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { WidgetComponentAction, WidgetComponentActionType } from '@home/components/widget/widget-container.component'; import { WidgetComponentAction, WidgetComponentActionType } from '@home/components/widget/widget-container.component';
import { TbPopoverComponent } from '@shared/components/popover.component';
@Component({ @Component({
selector: 'tb-dashboard', selector: 'tb-dashboard',
@ -136,6 +139,9 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
@Input() @Input()
parentDashboard?: IDashboardComponent = null; parentDashboard?: IDashboardComponent = null;
@Input()
popoverComponent?: TbPopoverComponent = null;
dashboardTimewindowChangedSubject: Subject<Timewindow> = new ReplaySubject<Timewindow>(); dashboardTimewindowChangedSubject: Subject<Timewindow> = new ReplaySubject<Timewindow>();
dashboardTimewindowChanged = this.dashboardTimewindowChangedSubject.asObservable().pipe( dashboardTimewindowChanged = this.dashboardTimewindowChangedSubject.asObservable().pipe(
@ -191,6 +197,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
ngOnInit(): void { ngOnInit(): void {
this.dashboardWidgets.parentDashboard = this.parentDashboard; this.dashboardWidgets.parentDashboard = this.parentDashboard;
this.dashboardWidgets.popoverComponent = this.popoverComponent;
if (!this.dashboardTimewindow) { if (!this.dashboardTimewindow) {
this.dashboardTimewindow = this.timeService.defaultTimewindow(); this.dashboardTimewindow = this.timeService.defaultTimewindow();
} }

View File

@ -28,7 +28,8 @@ import {
NgZone, NgZone,
OnChanges, OnChanges,
OnDestroy, OnDestroy,
OnInit, Renderer2, OnInit,
Renderer2,
SimpleChanges, SimpleChanges,
ViewChild, ViewChild,
ViewContainerRef, ViewContainerRef,
@ -39,12 +40,14 @@ import {
defaultLegendConfig, defaultLegendConfig,
LegendConfig, LegendConfig,
LegendData, LegendData,
LegendPosition, MobileActionResult, LegendPosition,
Widget, Widget,
WidgetActionDescriptor, WidgetActionDescriptor,
widgetActionSources, widgetActionSources,
WidgetActionType, WidgetActionType,
WidgetComparisonSettings, WidgetMobileActionDescriptor, WidgetMobileActionType, WidgetComparisonSettings,
WidgetMobileActionDescriptor,
WidgetMobileActionType,
WidgetResource, WidgetResource,
widgetType, widgetType,
WidgetTypeParameters WidgetTypeParameters
@ -65,7 +68,9 @@ import {
validateEntityId validateEntityId
} from '@core/utils'; } from '@core/utils';
import { import {
IDynamicWidgetComponent, ShowWidgetHeaderActionFunction, updateEntityParams, IDynamicWidgetComponent,
ShowWidgetHeaderActionFunction,
updateEntityParams,
WidgetContext, WidgetContext,
WidgetHeaderAction, WidgetHeaderAction,
WidgetInfo, WidgetInfo,
@ -109,9 +114,7 @@ import { MobileService } from '@core/services/mobile.service';
import { DialogService } from '@core/services/dialog.service'; import { DialogService } from '@core/services/dialog.service';
import { PopoverPlacement } from '@shared/components/popover.models'; import { PopoverPlacement } from '@shared/components/popover.models';
import { TbPopoverService } from '@shared/components/popover.service'; import { TbPopoverService } from '@shared/components/popover.service';
import { import { DASHBOARD_PAGE_COMPONENT_TOKEN } from '@home/components/tokens';
DASHBOARD_PAGE_COMPONENT_TOKEN
} from '@home/components/tokens';
@Component({ @Component({
selector: 'tb-widget', selector: 'tb-widget',
@ -1378,8 +1381,9 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
} }
] ]
}); });
const component = this.popoverService.displayPopover(trigger, this.renderer, const componentRef = this.popoverService.createPopoverRef(this.widgetContentContainer);
this.widgetContentContainer, this.dashboardPageComponent, preferredPlacement, hideOnClickOutside, const component = this.popoverService.displayPopoverWithComponentRef(componentRef, trigger, this.renderer,
this.dashboardPageComponent, preferredPlacement, hideOnClickOutside,
injector, injector,
{ {
embedded: true, embedded: true,
@ -1388,7 +1392,8 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
currentState: objToBase64([stateObject]), currentState: objToBase64([stateObject]),
dashboard, dashboard,
parentDashboard: this.widgetContext.parentDashboard ? parentDashboard: this.widgetContext.parentDashboard ?
this.widgetContext.parentDashboard : this.widgetContext.dashboard this.widgetContext.parentDashboard : this.widgetContext.dashboard,
popoverComponent: componentRef.instance
}, },
{width: popoverWidth, height: popoverHeight}, {width: popoverWidth, height: popoverHeight},
popoverStyle, popoverStyle,

View File

@ -16,8 +16,8 @@
import { GridsterComponent, GridsterConfig, GridsterItem, GridsterItemComponentInterface } from 'angular-gridster2'; import { GridsterComponent, GridsterConfig, GridsterItem, GridsterItemComponentInterface } from 'angular-gridster2';
import { import {
Datasource, datasourcesHasAggregation,
datasourcesHasAggregation, datasourcesHasOnlyComparisonAggregation, datasourcesHasOnlyComparisonAggregation,
FormattedData, FormattedData,
Widget, Widget,
WidgetPosition, WidgetPosition,
@ -25,14 +25,14 @@ import {
} from '@app/shared/models/widget.models'; } from '@app/shared/models/widget.models';
import { WidgetLayout, WidgetLayouts } from '@app/shared/models/dashboard.models'; import { WidgetLayout, WidgetLayouts } from '@app/shared/models/dashboard.models';
import { IDashboardWidget, WidgetAction, WidgetContext, WidgetHeaderAction } from './widget-component.models'; import { IDashboardWidget, WidgetAction, WidgetContext, WidgetHeaderAction } from './widget-component.models';
import { AggregationType, Timewindow } from '@shared/models/time/time.models'; import { Timewindow } from '@shared/models/time/time.models';
import { Observable, of, Subject } from 'rxjs'; import { Observable, of, Subject } from 'rxjs';
import { formattedDataFormDatasourceData, guid, isDefined, isEqual, isUndefined } from '@app/core/utils'; import { formattedDataFormDatasourceData, guid, isDefined, isEqual, isUndefined } from '@app/core/utils';
import { IterableDiffer, KeyValueDiffer } from '@angular/core'; import { IterableDiffer, KeyValueDiffer } from '@angular/core';
import { IAliasController, IStateController } from '@app/core/api/widget-api.models'; import { IAliasController, IStateController } from '@app/core/api/widget-api.models';
import { enumerable } from '@shared/decorators/enumerable'; import { enumerable } from '@shared/decorators/enumerable';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { TbPopoverComponent } from '@shared/components/popover.component';
export interface WidgetsData { export interface WidgetsData {
widgets: Array<Widget>; widgets: Array<Widget>;
@ -109,6 +109,8 @@ export class DashboardWidgets implements Iterable<DashboardWidget> {
parentDashboard?: IDashboardComponent; parentDashboard?: IDashboardComponent;
popoverComponent?: TbPopoverComponent;
[Symbol.iterator](): Iterator<DashboardWidget> { [Symbol.iterator](): Iterator<DashboardWidget> {
return this.activeDashboardWidgets[Symbol.iterator](); return this.activeDashboardWidgets[Symbol.iterator]();
} }
@ -174,7 +176,7 @@ export class DashboardWidgets implements Iterable<DashboardWidget> {
switch (record.operation) { switch (record.operation) {
case 'add': case 'add':
this.dashboardWidgets.push( this.dashboardWidgets.push(
new DashboardWidget(this.dashboard, record.widget, record.widgetLayout, this.parentDashboard) new DashboardWidget(this.dashboard, record.widget, record.widgetLayout, this.parentDashboard, this.popoverComponent)
); );
break; break;
case 'remove': case 'remove':
@ -190,7 +192,7 @@ export class DashboardWidgets implements Iterable<DashboardWidget> {
if (!isEqual(prevDashboardWidget.widget, record.widget) || if (!isEqual(prevDashboardWidget.widget, record.widget) ||
!isEqual(prevDashboardWidget.widgetLayout, record.widgetLayout)) { !isEqual(prevDashboardWidget.widgetLayout, record.widgetLayout)) {
this.dashboardWidgets[index] = new DashboardWidget(this.dashboard, record.widget, record.widgetLayout, this.dashboardWidgets[index] = new DashboardWidget(this.dashboard, record.widget, record.widgetLayout,
this.parentDashboard); this.parentDashboard, this.popoverComponent);
this.dashboardWidgets[index].highlighted = prevDashboardWidget.highlighted; this.dashboardWidgets[index].highlighted = prevDashboardWidget.highlighted;
this.dashboardWidgets[index].selected = prevDashboardWidget.selected; this.dashboardWidgets[index].selected = prevDashboardWidget.selected;
} else { } else {
@ -345,7 +347,7 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget {
customHeaderActions: Array<WidgetHeaderAction>; customHeaderActions: Array<WidgetHeaderAction>;
widgetActions: Array<WidgetAction>; widgetActions: Array<WidgetAction>;
widgetContext = new WidgetContext(this.dashboard, this, this.widget, this.parentDashboard); widgetContext = new WidgetContext(this.dashboard, this, this.widget, this.parentDashboard, this.popoverComponent);
widgetId: string; widgetId: string;
@ -388,7 +390,8 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget {
private dashboard: IDashboardComponent, private dashboard: IDashboardComponent,
public widget: Widget, public widget: Widget,
public widgetLayout?: WidgetLayout, public widgetLayout?: WidgetLayout,
private parentDashboard?: IDashboardComponent) { private parentDashboard?: IDashboardComponent,
private popoverComponent?: TbPopoverComponent) {
if (!widget.id) { if (!widget.id) {
widget.id = guid(); widget.id = guid();
} }

View File

@ -18,7 +18,8 @@ import { IDashboardComponent } from '@home/models/dashboard-component.models';
import { import {
DataSet, DataSet,
Datasource, Datasource,
DatasourceData, FormattedData, DatasourceData,
FormattedData,
JsonSettingsSchema, JsonSettingsSchema,
Widget, Widget,
WidgetActionDescriptor, WidgetActionDescriptor,
@ -37,7 +38,8 @@ import {
IStateController, IStateController,
IWidgetSubscription, IWidgetSubscription,
IWidgetUtils, IWidgetUtils,
RpcApi, StateParams, RpcApi,
StateParams,
SubscriptionEntityInfo, SubscriptionEntityInfo,
TimewindowFunctions, TimewindowFunctions,
WidgetActionsApi, WidgetActionsApi,
@ -113,7 +115,8 @@ export class WidgetContext {
constructor(public dashboard: IDashboardComponent, constructor(public dashboard: IDashboardComponent,
private dashboardWidget: IDashboardWidget, private dashboardWidget: IDashboardWidget,
private widget: Widget, private widget: Widget,
public parentDashboard?: IDashboardComponent) {} public parentDashboard?: IDashboardComponent,
public popoverComponent?: TbPopoverComponent) {}
get stateController(): IStateController { get stateController(): IStateController {
return this.parentDashboard ? this.parentDashboard.stateController : this.dashboard.stateController; return this.parentDashboard ? this.parentDashboard.stateController : this.dashboard.stateController;

View File

@ -57,6 +57,7 @@ import {
} from '@shared/components/popover.models'; } from '@shared/components/popover.models';
import { distinctUntilChanged, take, takeUntil } from 'rxjs/operators'; import { distinctUntilChanged, take, takeUntil } from 'rxjs/operators';
import { isNotEmptyStr, onParentScrollOrWindowResize } from '@core/utils'; import { isNotEmptyStr, onParentScrollOrWindowResize } from '@core/utils';
import { animate, AnimationBuilder, AnimationMetadata, style } from '@angular/animations';
export type TbPopoverTrigger = 'click' | 'focus' | 'hover' | null; export type TbPopoverTrigger = 'click' | 'focus' | 'hover' | null;
@ -304,6 +305,7 @@ export class TbPopoverDirective implements OnChanges, OnDestroy, AfterViewInit {
<div #popoverRoot [@popoverMotion]="tbAnimationState" <div #popoverRoot [@popoverMotion]="tbAnimationState"
(@popoverMotion.done)="animationDone()"> (@popoverMotion.done)="animationDone()">
<div <div
#popover
class="tb-popover" class="tb-popover"
[class.tb-popover-rtl]="dir === 'rtl'" [class.tb-popover-rtl]="dir === 'rtl'"
[ngClass]="classMap" [ngClass]="classMap"
@ -340,6 +342,7 @@ export class TbPopoverComponent implements OnDestroy, OnInit {
@ViewChild('overlay', { static: false }) overlay!: CdkConnectedOverlay; @ViewChild('overlay', { static: false }) overlay!: CdkConnectedOverlay;
@ViewChild('popoverRoot', { static: false }) popoverRoot!: ElementRef<HTMLElement>; @ViewChild('popoverRoot', { static: false }) popoverRoot!: ElementRef<HTMLElement>;
@ViewChild('popover', { static: false }) popover!: ElementRef<HTMLElement>;
tbContent: string | TemplateRef<void> | null = null; tbContent: string | TemplateRef<void> | null = null;
tbComponentFactory: ComponentFactory<any> | null = null; tbComponentFactory: ComponentFactory<any> | null = null;
@ -442,6 +445,7 @@ export class TbPopoverComponent implements OnDestroy, OnInit {
constructor( constructor(
public cdr: ChangeDetectorRef, public cdr: ChangeDetectorRef,
private renderer: Renderer2, private renderer: Renderer2,
private animationBuilder: AnimationBuilder,
@Optional() private directionality: Directionality @Optional() private directionality: Directionality
) {} ) {}
@ -532,6 +536,35 @@ export class TbPopoverComponent implements OnDestroy, OnInit {
}); });
} }
resize(width: string, height: string, animationDurationMs?: number) {
if (animationDurationMs && animationDurationMs > 0) {
const prevWidth = this.popover.nativeElement.offsetWidth;
const prevHeight = this.popover.nativeElement.offsetHeight;
const animationMetadata: AnimationMetadata[] = [style({width: prevWidth + 'px', height: prevHeight + 'px'}),
animate(animationDurationMs + 'ms', style({width, height}))];
const factory = this.animationBuilder.build(animationMetadata);
const player = factory.create(this.popover.nativeElement);
player.play();
const resize$ = new ResizeObserver(() => {
this.updatePosition();
});
resize$.observe(this.popover.nativeElement);
player.onDone(() => {
player.destroy();
resize$.disconnect();
this.setSize(width, height);
});
} else {
this.setSize(width, height);
}
}
private setSize(width: string, height: string) {
this.renderer.setStyle(this.popover.nativeElement, 'width', width);
this.renderer.setStyle(this.popover.nativeElement, 'height', height);
this.updatePosition();
}
updatePosition(): void { updatePosition(): void {
if (this.origin && this.overlay && this.overlay.overlayRef) { if (this.origin && this.overlay && this.overlay.overlayRef) {
this.overlay.overlayRef.updatePosition(); this.overlay.overlayRef.updatePosition();

View File

@ -16,8 +16,12 @@
import { import {
ComponentFactory, ComponentFactory,
ComponentFactoryResolver, ElementRef, Inject, ComponentFactoryResolver,
Injectable, Injector, ComponentRef,
ElementRef,
Inject,
Injectable,
Injector,
Renderer2, Renderer2,
Type, Type,
ViewContainerRef ViewContainerRef
@ -53,11 +57,23 @@ export class TbPopoverService {
} }
} }
createPopoverRef(hostView: ViewContainerRef): ComponentRef<TbPopoverComponent> {
return hostView.createComponent(this.componentFactory);
}
displayPopover<T>(trigger: Element, renderer: Renderer2, hostView: ViewContainerRef, displayPopover<T>(trigger: Element, renderer: Renderer2, hostView: ViewContainerRef,
componentType: Type<T>, preferredPlacement: PopoverPlacement = 'top', hideOnClickOutside = true, componentType: Type<T>, preferredPlacement: PopoverPlacement = 'top', hideOnClickOutside = true,
injector?: Injector, context?: any, overlayStyle: any = {}, popoverStyle: any = {}, style?: any, injector?: Injector, context?: any, overlayStyle: any = {}, popoverStyle: any = {}, style?: any,
showCloseButton = true): TbPopoverComponent { showCloseButton = true): TbPopoverComponent {
const componentRef = hostView.createComponent(this.componentFactory); const componentRef = this.createPopoverRef(hostView);
return this.displayPopoverWithComponentRef(componentRef, trigger, renderer, componentType, preferredPlacement, hideOnClickOutside,
injector, context, overlayStyle, popoverStyle, style, showCloseButton);
}
displayPopoverWithComponentRef<T>(componentRef: ComponentRef<TbPopoverComponent>, trigger: Element, renderer: Renderer2,
componentType: Type<T>, preferredPlacement: PopoverPlacement = 'top',
hideOnClickOutside = true, injector?: Injector, context?: any, overlayStyle: any = {},
popoverStyle: any = {}, style?: any, showCloseButton = true): TbPopoverComponent {
const component = componentRef.instance; const component = componentRef.instance;
this.popoverWithTriggers.push({ this.popoverWithTriggers.push({
trigger, trigger,