From 69f2a43063ddedba8399e90b935f60092283f735 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Fri, 30 Apr 2021 13:33:29 -0700 Subject: [PATCH] EventBus: add origin to all events and support nested EventBus (#33548) --- .../grafana-data/src/events/EventBus.test.ts | 10 ++-- packages/grafana-data/src/events/EventBus.ts | 54 +++++++++++-------- packages/grafana-data/src/events/common.ts | 7 +-- packages/grafana-data/src/events/types.ts | 28 ++++++++-- .../components/PanelChrome/PanelContext.ts | 11 ++-- .../src/components/PanelChrome/index.ts | 2 +- .../src/components/PieChart/PieChart.tsx | 16 ++---- packages/grafana-ui/src/components/index.ts | 1 + .../components/SubMenu/AnnotationPicker.tsx | 20 ++++--- .../dashboard/dashgrid/PanelChrome.tsx | 27 ++++++++-- .../features/dashboard/state/PanelModel.ts | 8 ++- .../panel/annolist/AnnoListPanel.test.tsx | 3 +- .../plugins/panel/debug/EventBusLogger.tsx | 37 ++++++------- 13 files changed, 136 insertions(+), 88 deletions(-) diff --git a/packages/grafana-data/src/events/EventBus.test.ts b/packages/grafana-data/src/events/EventBus.test.ts index b28e49053bd..e0b0ca34c15 100644 --- a/packages/grafana-data/src/events/EventBus.test.ts +++ b/packages/grafana-data/src/events/EventBus.test.ts @@ -1,4 +1,4 @@ -import { EventBusSrv, EventBusWithSource } from './EventBus'; +import { EventBusSrv } from './EventBus'; import { BusEvent, BusEventWithPayload } from './types'; import { eventFactory } from './eventFactory'; import { DataHoverEvent } from './common'; @@ -50,8 +50,8 @@ describe('EventBus', () => { describe('EventBusWithSource', () => { it('can add sources to the source path', () => { const bus = new EventBusSrv(); - const busWithSource = new EventBusWithSource(bus, 'foo'); - expect(busWithSource.source).toEqual('foo'); + const busWithSource = bus.newScopedBus('foo'); + expect((busWithSource as any).path).toEqual(['foo']); }); it('adds the source to the event payload', () => { @@ -60,11 +60,11 @@ describe('EventBus', () => { bus.subscribe(DataHoverEvent, (event) => events.push(event)); - const busWithSource = new EventBusWithSource(bus, 'foo'); + const busWithSource = bus.newScopedBus('foo'); busWithSource.publish({ type: DataHoverEvent.type }); expect(events.length).toEqual(1); - expect(events[0].payload.source).toEqual('foo'); + expect(events[0].origin).toEqual(busWithSource); }); }); diff --git a/packages/grafana-data/src/events/EventBus.ts b/packages/grafana-data/src/events/EventBus.ts index fd616adcba0..2dd3e7a55b2 100644 --- a/packages/grafana-data/src/events/EventBus.ts +++ b/packages/grafana-data/src/events/EventBus.ts @@ -1,6 +1,6 @@ import EventEmitter from 'eventemitter3'; import { Unsubscribable, Observable } from 'rxjs'; -import { PayloadWithSource } from './common'; +import { filter } from 'rxjs/operators'; import { EventBus, LegacyEmitter, @@ -9,7 +9,7 @@ import { LegacyEventHandler, BusEvent, AppEvent, - BusEventWithPayload, + EventFilterOptions, } from './types'; /** @@ -44,6 +44,10 @@ export class EventBusSrv implements EventBus, LegacyEmitter { }); } + newScopedBus(key: string, filter?: EventFilterOptions): EventBus { + return new ScopedEventBus([key], this, filter); + } + /** * Legacy functions */ @@ -94,40 +98,48 @@ export class EventBusSrv implements EventBus, LegacyEmitter { } /** - * @alpha - * * Wraps EventBus and adds a source to help with identifying if a subscriber should react to the event or not. */ -export class EventBusWithSource implements EventBus { - source: string; - eventBus: EventBus; +class ScopedEventBus implements EventBus { + // will be mutated by panel runners + filterConfig: EventFilterOptions; - constructor(eventBus: EventBus, source: string) { - this.eventBus = eventBus; - this.source = source; + // The path is not yet exposed, but can be used to indicate nested groups and support faster filtering + constructor(public path: string[], private eventBus: EventBus, filter?: EventFilterOptions) { + this.filterConfig = filter ?? { onlyLocal: false }; } publish(event: T): void { - const decoratedEvent = { - ...event, - ...{ payload: { ...event.payload, ...{ source: this.source } } }, - }; - this.eventBus.publish(decoratedEvent); + if (!event.origin) { + (event as any).origin = this; + } + this.eventBus.publish(event); } - subscribe(eventType: BusEventType, handler: BusEventHandler): Unsubscribable { - return this.eventBus.subscribe(eventType, handler); - } + filter = (event: BusEvent) => { + if (this.filterConfig.onlyLocal) { + return event.origin === this; + } + return true; + }; getStream(eventType: BusEventType): Observable { - return this.eventBus.getStream(eventType); + return this.eventBus.getStream(eventType).pipe(filter(this.filter)) as Observable; + } + + // syntax sugar + subscribe(typeFilter: BusEventType, handler: BusEventHandler): Unsubscribable { + return this.getStream(typeFilter).subscribe({ next: handler }); } removeAllListeners(): void { this.eventBus.removeAllListeners(); } - isOwnEvent(event: BusEventWithPayload): boolean { - return event.payload.source === this.source; + /** + * Creates a nested event bus structure + */ + newScopedBus(key: string, filter: EventFilterOptions): EventBus { + return new ScopedEventBus([...this.path, key], this, filter); } } diff --git a/packages/grafana-data/src/events/common.ts b/packages/grafana-data/src/events/common.ts index 280666091a9..071cbf25b7c 100644 --- a/packages/grafana-data/src/events/common.ts +++ b/packages/grafana-data/src/events/common.ts @@ -2,12 +2,7 @@ import { DataFrame } from '../types'; import { BusEventWithPayload } from './types'; /** @alpha */ -export interface PayloadWithSource { - source?: string; // source from where the event originates -} - -/** @alpha */ -export interface DataHoverPayload extends PayloadWithSource { +export interface DataHoverPayload { raw: any; // Original mouse event (includes pageX etc) x: Record; // { time: 5678 }, diff --git a/packages/grafana-data/src/events/types.ts b/packages/grafana-data/src/events/types.ts index 63b4738623a..7deead3970d 100644 --- a/packages/grafana-data/src/events/types.ts +++ b/packages/grafana-data/src/events/types.ts @@ -7,6 +7,7 @@ import { Unsubscribable, Observable } from 'rxjs'; export interface BusEvent { readonly type: string; readonly payload?: any; + readonly origin?: EventBus; } /** @@ -52,6 +53,14 @@ export interface BusEventHandler { (event: T): void; } +/** + * @alpha + * Main minimal interface + */ +export interface EventFilterOptions { + onlyLocal: boolean; +} + /** * @alpha * Main minimal interface @@ -62,20 +71,29 @@ export interface EventBus { */ publish(event: T): void; - /** - * Subscribe to single event - */ - subscribe(eventType: BusEventType, handler: BusEventHandler): Unsubscribable; - /** * Get observable of events */ getStream(eventType: BusEventType): Observable; + /** + * Subscribe to an event stream + * + * This function is a wrapper around the `getStream(...)` function + */ + subscribe(eventType: BusEventType, handler: BusEventHandler): Unsubscribable; + /** * Remove all event subscriptions */ removeAllListeners(): void; + + /** + * Returns a new bus scoped that knows where it exists in a heiarchy + * + * @internal -- This is included for internal use only should not be used directly + */ + newScopedBus(key: string, filter: EventFilterOptions): EventBus; } /** diff --git a/packages/grafana-ui/src/components/PanelChrome/PanelContext.ts b/packages/grafana-ui/src/components/PanelChrome/PanelContext.ts index 2436c0ae885..33da42632ee 100644 --- a/packages/grafana-ui/src/components/PanelChrome/PanelContext.ts +++ b/packages/grafana-ui/src/components/PanelChrome/PanelContext.ts @@ -1,11 +1,14 @@ -import { EventBusWithSource } from '@grafana/data'; +import { EventBusSrv, EventBus } from '@grafana/data'; import React from 'react'; -interface PanelContext { - eventBus?: EventBusWithSource; +/** @alpha */ +export interface PanelContext { + eventBus: EventBus; } -const PanelContextRoot = React.createContext({}); +const PanelContextRoot = React.createContext({ + eventBus: new EventBusSrv(), +}); /** * @alpha diff --git a/packages/grafana-ui/src/components/PanelChrome/index.ts b/packages/grafana-ui/src/components/PanelChrome/index.ts index 7883b128a0e..ca06b467a8c 100644 --- a/packages/grafana-ui/src/components/PanelChrome/index.ts +++ b/packages/grafana-ui/src/components/PanelChrome/index.ts @@ -37,4 +37,4 @@ export { ErrorIndicatorProps as PanelChromeErrorIndicatorProps, } from './ErrorIndicator'; -export { usePanelContext, PanelContextProvider } from './PanelContext'; +export { usePanelContext, PanelContextProvider, PanelContext } from './PanelContext'; diff --git a/packages/grafana-ui/src/components/PieChart/PieChart.tsx b/packages/grafana-ui/src/components/PieChart/PieChart.tsx index 78cfe6b4f34..e08be6d2e00 100644 --- a/packages/grafana-ui/src/components/PieChart/PieChart.tsx +++ b/packages/grafana-ui/src/components/PieChart/PieChart.tsx @@ -143,25 +143,17 @@ function useSliceHighlightState() { const { eventBus } = usePanelContext(); useEffect(() => { - if (!eventBus) { - return; - } - const setHighlightedSlice = (event: DataHoverEvent) => { - if (eventBus.isOwnEvent(event)) { - setHighlightedTitle(event.payload.dataId); - } + setHighlightedTitle(event.payload.dataId); }; const resetHighlightedSlice = (event: DataHoverClearEvent) => { - if (eventBus.isOwnEvent(event)) { - setHighlightedTitle(undefined); - } + setHighlightedTitle(undefined); }; const subs = new Subscription() - .add(eventBus.subscribe(DataHoverEvent, setHighlightedSlice)) - .add(eventBus.subscribe(DataHoverClearEvent, resetHighlightedSlice)); + .add(eventBus.getStream(DataHoverEvent).subscribe({ next: setHighlightedSlice })) + .add(eventBus.getStream(DataHoverClearEvent).subscribe({ next: resetHighlightedSlice })); return () => { subs.unsubscribe(); diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index 14461458342..eb4a7cb6633 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -95,6 +95,7 @@ export { PanelChromeErrorIndicator, PanelChromeErrorIndicatorProps, PanelContextProvider, + PanelContext, usePanelContext, } from './PanelChrome'; export { VizLayout, VizLayoutComponentType, VizLayoutLegendProps, VizLayoutProps } from './VizLayout/VizLayout'; diff --git a/public/app/features/dashboard/components/SubMenu/AnnotationPicker.tsx b/public/app/features/dashboard/components/SubMenu/AnnotationPicker.tsx index a3147acba91..01e093f490d 100644 --- a/public/app/features/dashboard/components/SubMenu/AnnotationPicker.tsx +++ b/public/app/features/dashboard/components/SubMenu/AnnotationPicker.tsx @@ -18,15 +18,19 @@ export const AnnotationPicker = ({ annotation, events, onEnabledChanged }: Annot const onCancel = () => getDashboardQueryRunner().cancel(annotation); useEffect(() => { - const started = events.subscribe(AnnotationQueryStarted, (event) => { - if (event.payload === annotation) { - setLoading(true); - } + const started = events.getStream(AnnotationQueryStarted).subscribe({ + next: (event) => { + if (event.payload === annotation) { + setLoading(true); + } + }, }); - const stopped = events.subscribe(AnnotationQueryFinished, (event) => { - if (event.payload === annotation) { - setLoading(false); - } + const stopped = events.getStream(AnnotationQueryFinished).subscribe({ + next: (event) => { + if (event.payload === annotation) { + setLoading(false); + } + }, }); return () => { diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx index 98c99c983e4..9c4bf9324e0 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx @@ -4,7 +4,7 @@ import classNames from 'classnames'; import { Subscription } from 'rxjs'; // Components import { PanelHeader } from './PanelHeader/PanelHeader'; -import { ErrorBoundary, PanelContextProvider } from '@grafana/ui'; +import { ErrorBoundary, PanelContextProvider, PanelContext } from '@grafana/ui'; // Utils & Services import { getTimeSrv, TimeSrv } from '../services/TimeSrv'; import { applyPanelTimeOverrides } from 'app/features/dashboard/utils/panel'; @@ -15,7 +15,8 @@ import { DashboardModel, PanelModel } from '../state'; import { PANEL_BORDER } from 'app/core/constants'; import { AbsoluteTimeRange, - EventBusWithSource, + EventBusSrv, + EventFilterOptions, FieldConfigSource, getDefaultTimeRange, LoadingState, @@ -47,22 +48,34 @@ export interface State { renderCounter: number; errorMessage?: string; refreshWhenInView: boolean; - eventBus: EventBusWithSource; + context: PanelContext; data: PanelData; } export class PanelChrome extends Component { private readonly timeSrv: TimeSrv = getTimeSrv(); private subs = new Subscription(); + private eventFilter: EventFilterOptions = { onlyLocal: true }; constructor(props: Props) { super(props); + // Can this eventBus be on PanelModel? + // when we have more complex event filtering, that may be a better option + const eventBus = props.dashboard.events + ? props.dashboard.events.newScopedBus( + `panel:${props.panel.id}`, // panelID + this.eventFilter + ) + : new EventBusSrv(); + this.state = { isFirstLoad: true, renderCounter: 0, refreshWhenInView: false, - eventBus: new EventBusWithSource(props.dashboard.events, `panel-${props.panel.id}`), + context: { + eventBus, + }, data: this.getInitialPanelDataState(), }; } @@ -297,10 +310,14 @@ export class PanelChrome extends Component { }); const panelOptions = panel.getOptions(); + // Update the event filter (dashboard settings may have changed) + // Yes this is called ever render for a function that is triggered on every mouse move + this.eventFilter.onlyLocal = dashboard.graphTooltip === 0; + return ( <>
- + = {}; legend?: { show: boolean; sort?: string; sortDesc?: boolean }; plugin?: PanelPlugin; dataSupport?: PanelPluginDataSupport; + /** + * The PanelModel event bus only used for internal and legacy angular support. + * The EventBus passed to panels is based on the dashboard event model. + */ + events: EventBusSrv; + private queryRunner?: PanelQueryRunner; constructor(model: any) { diff --git a/public/app/plugins/panel/annolist/AnnoListPanel.test.tsx b/public/app/plugins/panel/annolist/AnnoListPanel.test.tsx index 246896e8656..576e6f834de 100644 --- a/public/app/plugins/panel/annolist/AnnoListPanel.test.tsx +++ b/public/app/plugins/panel/annolist/AnnoListPanel.test.tsx @@ -55,10 +55,11 @@ async function setupTestContext({ const props: Props = { data: { state: LoadingState.Done, timeRange: getDefaultTimeRange(), series: [] }, eventBus: { + subscribe: jest.fn(), getStream: jest.fn(), publish: jest.fn(), removeAllListeners: jest.fn(), - subscribe: jest.fn(), + newScopedBus: jest.fn(), }, fieldConfig: ({} as unknown) as FieldConfigSource, height: 400, diff --git a/public/app/plugins/panel/debug/EventBusLogger.tsx b/public/app/plugins/panel/debug/EventBusLogger.tsx index 99aad27db2a..b38824a8cfb 100644 --- a/public/app/plugins/panel/debug/EventBusLogger.tsx +++ b/public/app/plugins/panel/debug/EventBusLogger.tsx @@ -3,12 +3,10 @@ import { CustomScrollbar } from '@grafana/ui'; import { BusEvent, CircularVector, - DataHoverPayload, DataHoverEvent, DataHoverClearEvent, DataSelectEvent, EventBus, - BusEventHandler, } from '@grafana/data'; import { PartialObserver, Unsubscribable } from 'rxjs'; @@ -24,22 +22,25 @@ interface State { interface BusEventEx { key: number; type: string; - payload: DataHoverPayload; + path: string; + payload: any; } let counter = 100; export class EventBusLoggerPanel extends PureComponent { history = new CircularVector({ capacity: 40, append: 'head' }); - subs: Unsubscribable[] = []; + subs: Unsubscribable[]; constructor(props: Props) { super(props); this.state = { counter }; - this.subs.push(props.eventBus.subscribe(DataHoverEvent, this.hoverHandler)); - props.eventBus.getStream(DataHoverClearEvent).subscribe(this.eventObserver); - props.eventBus.getStream(DataSelectEvent).subscribe(this.eventObserver); + const subs: Unsubscribable[] = []; + subs.push(props.eventBus.getStream(DataHoverEvent).subscribe(this.eventObserver)); + subs.push(props.eventBus.getStream(DataHoverClearEvent).subscribe(this.eventObserver)); + subs.push(props.eventBus.getStream(DataSelectEvent).subscribe(this.eventObserver)); + this.subs = subs; } componentWillUnmount() { @@ -48,17 +49,17 @@ export class EventBusLoggerPanel extends PureComponent { } } - hoverHandler: BusEventHandler = (event: DataHoverEvent) => { - this.history.add({ - key: counter++, - type: event.type, - payload: event.payload, - }); - this.setState({ counter }); - }; - eventObserver: PartialObserver = { - next: (v: BusEvent) => {}, + next: (event: BusEvent) => { + const origin = event.origin as any; + this.history.add({ + key: counter++, + type: event.type, + path: origin?.path, + payload: event.payload, + }); + this.setState({ counter }); + }, }; render() { @@ -66,7 +67,7 @@ export class EventBusLoggerPanel extends PureComponent { {this.history.map((v, idx) => (
- {v.key} {v.type} / X:{JSON.stringify(v.payload.x)} / Y:{JSON.stringify(v.payload.y)} + {JSON.stringify(v.path)} {v.type} / X:{JSON.stringify(v.payload.x)} / Y:{JSON.stringify(v.payload.y)}
))}