import { v4 as uuidv4 } from 'uuid' ; import { Subscription } from 'rxjs' ; import { ActionStorage } from './dynamic_action_storage' ; import { UiActionsActionDefinition as ActionDefinition } from '../../../../../src/plugins/ui_actions/public' ; import { defaultState , transitions , selectors , State } from './dynamic_action_manager_state' ; import { StateContainer , createStateContainer , } from '../../../../../src/plugins/kibana_utils/common' ; import { StartContract } from '../plugin' ; import { SerializedAction , SerializedEvent } from './types' ; import { dynamicActionGrouping } from './dynamic_action_grouping' ; const compareEvents = ( a , b ) => { if ( a . length !== b . length ) return false ; for ( let i = 0 ; i < a . length ; i ++ ) if ( a [ i ] . eventId !== b [ i ] . eventId ) return false ; return true ; } ; export type DynamicActionManagerState = State ; export interface DynamicActionManagerParams { storage : ActionStorage ; uiActions : Pick < StartContract , 'registerAction' | 'attachAction' | 'unregisterAction' | 'detachAction' | 'hasAction' | 'getActionFactory' | 'hasActionFactory' > ; isCompatible : < C = unknown > ( context : C ) => Promise < boolean > ; } export class DynamicActionManager { static idPrefixCounter = 0 ; private readonly idPrefix = `D_ACTION_${ DynamicActionManager . idPrefixCounter ++ }_` ; private stopped : boolean = false ; private reloadSubscription ? : Subscription ; /**
     * UI State of the dynamic action manager.
     */ protected readonly ui = createStateContainer ( defaultState , transitions , selectors ) ; constructor ( protected readonly params ) { } protected getEvent ( eventId ) { const oldEvent = this . ui . selectors . getEvent ( eventId ) ; if ( ! oldEvent ) throw new Error ( `Could not find event [eventId = ${ eventId }].` ) ; return oldEvent ; } /**
     * We prefix action IDs with a unique `.idPrefix`, so we can render the
     * same dashboard twice on the screen.
     */ protected generateActionId ( eventId ) { return this . idPrefix + eventId ; } protected reviveAction ( event ) { const { eventId , triggers , action } = event ; const { uiActions , isCompatible } = this . params ; const actionId = this . generateActionId ( eventId ) ; if ( ! uiActions . hasActionFactory ( action . factoryId ) ) { console . warn ( `Action factory for action [action.factoryId = ${ action . factoryId }] doesn't exist. Skipping action [action.name = ${ action . name }] revive.` ) ; return ; } const factory = uiActions . getActionFactory ( event . action . factoryId ) ; const actionDefinition = factory . create ( action as SerializedAction ) ; uiActions . registerAction ( { ... actionDefinition , id : actionId , grouping : dynamicActionGrouping , isCompatible : async ( context ) => { if ( ! ( await isCompatible ( context ) ) ) return false ; if ( ! actionDefinition . isCompatible ) return true ; return actionDefinition . isCompatible ( context ) ; } , } ) ; const supportedTriggers = factory . supportedTriggers ( ) ; for ( const trigger of triggers ) { if ( ! supportedTriggers . includes ( trigger ) ) throw new Error ( `Can't attach [action=${ actionId }] to [trigger=${ trigger }]. Supported triggers for this action: ${ supportedTriggers . join ( ',' ) }` ) ; uiActions . attachAction ( trigger as string , actionId ) ; } } protected killAction ( { eventId , triggers } ) { const { uiActions } = this . params ; const actionId = this . generateActionId ( eventId ) ; if ( ! uiActions . hasAction ( actionId ) ) return ; for ( const trigger of triggers ) uiActions . detachAction ( trigger , actionId ) ; uiActions . unregisterAction ( actionId ) ; } private syncId = 0 ; /**
     * This function is called every time stored events might have changed not by
     * us. For example, when in edit mode on dashboard user presses "back" button
     * in the browser, then contents of storage changes.
     */ private onSync = ( ) => { if ( this . stopped ) return ; ( async ( ) => { const syncId = ++ this . syncId ; const events = await this . params . storage . list ( ) ; if ( this . stopped ) return ; if ( syncId !== this . syncId ) return ; if ( compareEvents ( events , this . ui . get ( ) . events ) ) return ; for ( const event of this . ui . get ( ) . events ) this . killAction ( event ) ; for ( const event of events ) this . reviveAction ( event ) ; this . ui . transitions . finishFetching ( events ) ; } ) ( ) . catch ( ( error ) => { console . log ( 'Dynamic action manager storage reload failed.' ) ; console . error ( error ) ; } ) ; } ; /**
     * Read-only state container of dynamic action manager. Use it to perform all
     * *read* operations.
     */ public readonly state : StateContainer < State > = this . ui ; /**
     * 1. Loads all events from  @type {DynamicActionStorage} storage.
     * 2. Creates actions for each event in `ui_actions` registry.
     * 3. Adds events to UI state.
     * 4. Does nothing if dynamic action manager was stopped or if event fetching
     *    is already taking place.
     */ public async start ( ) { if ( this . stopped ) return ; if ( this . ui . get ( ) . isFetchingEvents ) return ; this . ui . transitions . startFetching ( ) ; try { const events = await this . params . storage . list ( ) ; for ( const event of events ) this . reviveAction ( event ) ; this . ui . transitions . finishFetching ( events ) ; } catch ( error ) { this . ui . transitions . failFetching ( error instanceof Error ? error : { message : String ( error ) } ) ; throw error ; } if ( this . params . storage . reload$ ) { this . reloadSubscription = this . params . storage . reload$ . subscribe ( this . onSync ) ; } } /**
     * 1. Removes all events from `ui_actions` registry.
     * 2. Puts dynamic action manager is stopped state.
     */ public async stop ( ) { this . stopped = true ; const events = await this . params . storage . list ( ) ; for ( const event of events ) { this . killAction ( event ) ; } if ( this . reloadSubscription ) { this . reloadSubscription . unsubscribe ( ) ; } } param action param triggers public async createEvent ( action , triggers ) { if ( ! triggers . length ) { throw new Error ( 'No triggers selected for event.' ) ; } const event = { eventId : uuidv4 ( ) , triggers , action , } ; this . ui . transitions . addEvent ( event ) ; try { await this . params . storage . create ( event ) ; this . reviveAction ( event ) ; } catch ( error ) { this . ui . transitions . removeEvent ( event . eventId ) ; throw error ; } } param eventId param action param triggers public async updateEvent ( eventId , action , triggers ) { const event = { eventId , triggers , action , } ; const oldEvent = this . getEvent ( eventId ) ; this . killAction ( oldEvent ) ; this . reviveAction ( event ) ; this . ui . transitions . replaceEvent ( event ) ; try { await this . params . storage . update ( event ) ; } catch ( error ) { this . killAction ( event ) ; this . reviveAction ( oldEvent ) ; this . ui . transitions . replaceEvent ( oldEvent ) ; throw error ; } } param eventId public async deleteEvent ( eventId ) { const event = this . getEvent ( eventId ) ; this . killAction ( event ) ; this . ui . transitions . removeEvent ( eventId ) ; try { await this . params . storage . remove ( eventId ) ; } catch ( error ) { this . reviveAction ( event ) ; this . ui . transitions . addEvent ( event ) ; throw error ; } } param eventIds public async deleteEvents ( eventIds ) { await Promise . all ( eventIds . map ( this . deleteEvent . bind ( this ) ) ) ; } }