import { useEffect } from 'react'; import { Observer, Subject, Subscription, Unsubscribable } from 'rxjs'; import { v4 as uuidv4 } from 'uuid'; import { BusEvent, BusEventHandler, BusEventType, EventBusSrv } from '@grafana/data'; import { useForceUpdate } from '@grafana/ui'; import { SceneVariableDependencyConfigLike } from '../variables/types'; import { SceneComponentWrapper } from './SceneComponentWrapper'; import { SceneObjectStateChangedEvent } from './events'; import { SceneObject, SceneComponent, SceneObjectState, SceneObjectUrlSyncHandler } from './types'; import { cloneSceneObject, forEachSceneObjectInState } from './utils'; export abstract class SceneObjectBase implements SceneObject { private _isActive = false; private _subject = new Subject(); private _state: TState; private _events = new EventBusSrv(); /** Incremented in SceneComponentWrapper, useful for tests and rendering optimizations */ protected _renderCount = 0; protected _parent?: SceneObject; protected _subs = new Subscription(); protected _variableDependency: SceneVariableDependencyConfigLike | undefined; protected _urlSync: SceneObjectUrlSyncHandler | undefined; public constructor(state: TState) { if (!state.key) { state.key = uuidv4(); } this._state = Object.freeze(state); this._subject.next(state); this.setParent(); } /** Current state */ public get state(): TState { return this._state; } /** True if currently being active (ie displayed for visual objects) */ public get isActive(): boolean { return this._isActive; } /** Returns the parent, undefined for root object */ public get parent(): SceneObject | undefined { return this._parent; } /** Returns variable dependency config */ public get variableDependency(): SceneVariableDependencyConfigLike | undefined { return this._variableDependency; } /** Returns url sync config */ public get urlSync(): SceneObjectUrlSyncHandler | undefined { return this._urlSync; } /** * Used in render functions when rendering a SceneObject. * Wraps the component in an EditWrapper that handles edit mode */ public get Component(): SceneComponent { return SceneComponentWrapper; } /** * Temporary solution, should be replaced by declarative options */ public get Editor(): SceneComponent { return ((this as any).constructor['Editor'] ?? (() => null)) as SceneComponent; } private setParent() { forEachSceneObjectInState(this._state, (child) => (child._parent = this)); } /** * Subscribe to the scene state subject **/ public subscribeToState(observerOrNext?: Partial>): Subscription { return this._subject.subscribe(observerOrNext); } /** * Subscribe to the scene event **/ public subscribeToEvent(eventType: BusEventType, handler: BusEventHandler): Unsubscribable { return this._events.subscribe(eventType, handler); } public setState(update: Partial) { const prevState = this._state; const newState: TState = { ...this._state, ...update, }; this._state = Object.freeze(newState); this.setParent(); this._subject.next(newState); // Bubble state change event. This is event is subscribed to by UrlSyncManager and UndoManager this.publishEvent( new SceneObjectStateChangedEvent({ prevState, newState, partialUpdate: update, changedObject: this, }), true ); } /* * Publish an event and optionally bubble it up the scene **/ public publishEvent(event: BusEvent, bubble?: boolean) { this._events.publish(event); if (bubble && this.parent) { this.parent.publishEvent(event, bubble); } } public getRoot(): SceneObject { return !this._parent ? this : this._parent.getRoot(); } /** * Called by the SceneComponentWrapper when the react component is mounted */ public activate() { this._isActive = true; const { $data, $variables } = this.state; if ($data && !$data.isActive) { $data.activate(); } if ($variables && !$variables.isActive) { $variables.activate(); } } /** * Called by the SceneComponentWrapper when the react component is unmounted */ public deactivate(): void { this._isActive = false; const { $data, $variables } = this.state; if ($data && $data.isActive) { $data.deactivate(); } if ($variables && $variables.isActive) { $variables.deactivate(); } // Clear subscriptions and listeners this._events.removeAllListeners(); this._subs.unsubscribe(); this._subs = new Subscription(); this._subject.complete(); this._subject = new Subject(); } /** * Utility hook to get and subscribe to state */ public useState() { // eslint-disable-next-line react-hooks/rules-of-hooks return useSceneObjectState(this); } /** Force a re-render, should only be needed when variable values change */ public forceRender(): void { this.setState({}); } /** * Will create new SceneObject with shallow-cloned state, but all state items of type SceneObject are deep cloned */ public clone(withState?: Partial): this { return cloneSceneObject(this, withState); } } /** * This hook is always returning model.state instead of a useState that remembers the last state emitted on the subject * The reason for this is so that if the model instance change this function will always return the latest state. */ function useSceneObjectState(model: SceneObjectBase): TState { const forceUpdate = useForceUpdate(); useEffect(() => { const s = model.subscribeToState({ next: forceUpdate }); return () => s.unsubscribe(); }, [model, forceUpdate]); return model.state; }