|
|
|
@ -1,41 +1,48 @@ |
|
|
|
|
import { useEffect } from 'react'; |
|
|
|
|
import { Observer, Subject, Subscription } from 'rxjs'; |
|
|
|
|
import { Observer, Subject, Subscription, Unsubscribable } from 'rxjs'; |
|
|
|
|
import { v4 as uuidv4 } from 'uuid'; |
|
|
|
|
|
|
|
|
|
import { EventBusSrv } from '@grafana/data'; |
|
|
|
|
import { BusEvent, BusEventHandler, BusEventType, EventBusSrv } from '@grafana/data'; |
|
|
|
|
import { useForceUpdate } from '@grafana/ui'; |
|
|
|
|
|
|
|
|
|
import { SceneComponentWrapper } from './SceneComponentWrapper'; |
|
|
|
|
import { SceneObjectStateChangedEvent } from './events'; |
|
|
|
|
import { |
|
|
|
|
SceneDataState, |
|
|
|
|
SceneObject, |
|
|
|
|
SceneComponent, |
|
|
|
|
SceneEditor, |
|
|
|
|
SceneTimeRange, |
|
|
|
|
isSceneObject, |
|
|
|
|
SceneObjectState, |
|
|
|
|
SceneLayoutChild, |
|
|
|
|
} from './types'; |
|
|
|
|
import { SceneDataState, SceneObject, SceneComponent, SceneEditor, SceneTimeRange, SceneObjectState } from './types'; |
|
|
|
|
|
|
|
|
|
export abstract class SceneObjectBase<TState extends SceneObjectState = {}> implements SceneObject<TState> { |
|
|
|
|
subject = new Subject<TState>(); |
|
|
|
|
state: TState; |
|
|
|
|
parent?: SceneObjectBase<SceneObjectState>; |
|
|
|
|
subs = new Subscription(); |
|
|
|
|
isActive?: boolean; |
|
|
|
|
events = new EventBusSrv(); |
|
|
|
|
private _isActive = false; |
|
|
|
|
private _subject = new Subject<TState>(); |
|
|
|
|
private _state: TState; |
|
|
|
|
private _events = new EventBusSrv(); |
|
|
|
|
|
|
|
|
|
protected _parent?: SceneObject; |
|
|
|
|
protected subs = new Subscription(); |
|
|
|
|
|
|
|
|
|
constructor(state: TState) { |
|
|
|
|
if (!state.key) { |
|
|
|
|
state.key = uuidv4(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
this.state = state; |
|
|
|
|
this.subject.next(state); |
|
|
|
|
this._state = state; |
|
|
|
|
this._subject.next(state); |
|
|
|
|
this.setParent(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** Current state */ |
|
|
|
|
get state(): TState { |
|
|
|
|
return this._state; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** True if currently being active (ie displayed for visual objects) */ |
|
|
|
|
get isActive(): boolean { |
|
|
|
|
return this._isActive; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** Returns the parent, undefined for root object */ |
|
|
|
|
get parent(): SceneObject | undefined { |
|
|
|
|
return this._parent; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Used in render functions when rendering a SceneObject. |
|
|
|
|
* Wraps the component in an EditWrapper that handles edit mode |
|
|
|
@ -52,69 +59,105 @@ export abstract class SceneObjectBase<TState extends SceneObjectState = {}> impl |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private setParent() { |
|
|
|
|
for (const propValue of Object.values(this.state)) { |
|
|
|
|
if (isSceneObject(propValue)) { |
|
|
|
|
propValue.parent = this; |
|
|
|
|
for (const propValue of Object.values(this._state)) { |
|
|
|
|
if (propValue instanceof SceneObjectBase) { |
|
|
|
|
propValue._parent = this; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (Array.isArray(propValue)) { |
|
|
|
|
for (const child of propValue) { |
|
|
|
|
if (isSceneObject(child)) { |
|
|
|
|
child.parent = this; |
|
|
|
|
if (child instanceof SceneObjectBase) { |
|
|
|
|
child._parent = this; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** This function implements the Subscribable<TState> interface */ |
|
|
|
|
subscribe(observer: Partial<Observer<TState>>) { |
|
|
|
|
return this.subject.subscribe(observer); |
|
|
|
|
/** |
|
|
|
|
* Subscribe to the scene state subject |
|
|
|
|
**/ |
|
|
|
|
subscribeToState(observerOrNext?: Partial<Observer<TState>>): Subscription { |
|
|
|
|
return this._subject.subscribe(observerOrNext); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Subscribe to the scene event |
|
|
|
|
**/ |
|
|
|
|
subscribeToEvent<T extends BusEvent>(eventType: BusEventType<T>, handler: BusEventHandler<T>): Unsubscribable { |
|
|
|
|
return this._events.subscribe(eventType, handler); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
setState(update: Partial<TState>) { |
|
|
|
|
const prevState = this.state; |
|
|
|
|
this.state = { |
|
|
|
|
...this.state, |
|
|
|
|
const prevState = this._state; |
|
|
|
|
this._state = { |
|
|
|
|
...this._state, |
|
|
|
|
...update, |
|
|
|
|
}; |
|
|
|
|
this.setParent(); |
|
|
|
|
this.subject.next(this.state); |
|
|
|
|
this._subject.next(this._state); |
|
|
|
|
|
|
|
|
|
// broadcast state change. This is event is subscribed to by UrlSyncManager and UndoManager
|
|
|
|
|
this.getRoot().events.publish( |
|
|
|
|
// Bubble state change event. This is event is subscribed to by UrlSyncManager and UndoManager
|
|
|
|
|
this.publishEvent( |
|
|
|
|
new SceneObjectStateChangedEvent({ |
|
|
|
|
prevState, |
|
|
|
|
newState: this.state, |
|
|
|
|
newState: this._state, |
|
|
|
|
partialUpdate: update, |
|
|
|
|
changedObject: this, |
|
|
|
|
}) |
|
|
|
|
}), |
|
|
|
|
true |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private getRoot(): SceneObject { |
|
|
|
|
return !this.parent ? this : this.parent.getRoot(); |
|
|
|
|
/* |
|
|
|
|
* Publish an event and optionally bubble it up the scene |
|
|
|
|
**/ |
|
|
|
|
publishEvent(event: BusEvent, bubble?: boolean) { |
|
|
|
|
this._events.publish(event); |
|
|
|
|
|
|
|
|
|
if (bubble && this.parent) { |
|
|
|
|
this.parent.publishEvent(event, bubble); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
getRoot(): SceneObject { |
|
|
|
|
return !this._parent ? this : this._parent.getRoot(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
activate() { |
|
|
|
|
this.isActive = true; |
|
|
|
|
this._isActive = true; |
|
|
|
|
|
|
|
|
|
const { $data, $variables } = this.state; |
|
|
|
|
|
|
|
|
|
const { $data } = this.state; |
|
|
|
|
if ($data && !$data.isActive) { |
|
|
|
|
$data.activate(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if ($variables && !$variables.isActive) { |
|
|
|
|
$variables.activate(); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
deactivate(): void { |
|
|
|
|
this.isActive = false; |
|
|
|
|
this._isActive = false; |
|
|
|
|
|
|
|
|
|
const { $data, $variables } = this.state; |
|
|
|
|
|
|
|
|
|
const { $data } = 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<TState>(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
useState() { |
|
|
|
@ -171,7 +214,7 @@ export abstract class SceneObjectBase<TState extends SceneObjectState = {}> impl |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Will create new SceneItem with shalled cloned state, but all states items of type SceneItem are deep cloned |
|
|
|
|
* Will create new SceneItem with shalled cloned state, but all states items of type SceneObject are deep cloned |
|
|
|
|
*/ |
|
|
|
|
clone(withState?: Partial<TState>): this { |
|
|
|
|
const clonedState = { ...this.state }; |
|
|
|
@ -182,15 +225,19 @@ export abstract class SceneObjectBase<TState extends SceneObjectState = {}> impl |
|
|
|
|
if (propValue instanceof SceneObjectBase) { |
|
|
|
|
clonedState[key] = propValue.clone(); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Clone layout children
|
|
|
|
|
if ('children' in this.state) { |
|
|
|
|
const newChildren: SceneLayoutChild[] = []; |
|
|
|
|
for (const child of this.state.children) { |
|
|
|
|
newChildren.push(child.clone()); |
|
|
|
|
// Clone scene objects in arrays
|
|
|
|
|
if (Array.isArray(propValue)) { |
|
|
|
|
const newArray: any = []; |
|
|
|
|
for (const child of propValue) { |
|
|
|
|
if (child instanceof SceneObjectBase) { |
|
|
|
|
newArray.push(child.clone()); |
|
|
|
|
} else { |
|
|
|
|
newArray.push(child); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
clonedState[key] = newArray; |
|
|
|
|
} |
|
|
|
|
(clonedState as any).children = newChildren; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
Object.assign(clonedState, withState); |
|
|
|
@ -207,7 +254,7 @@ function useSceneObjectState<TState extends SceneObjectState>(model: SceneObject |
|
|
|
|
const forceUpdate = useForceUpdate(); |
|
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
|
const s = model.subject.subscribe(forceUpdate); |
|
|
|
|
const s = model.subscribeToState({ next: forceUpdate }); |
|
|
|
|
return () => s.unsubscribe(); |
|
|
|
|
}, [model, forceUpdate]); |
|
|
|
|
|
|
|
|
|