import useObservable from 'react-use/lib/useObservable' ; import { BehaviorSubject } from 'rxjs' ; import { map } from 'rxjs/operators' ; import type { SerializableRecord } from '@kbn/utility-types' ; import { PublicDrilldownManagerProps , DrilldownManagerDependencies , DrilldownTemplate , } from '../types' ; import { ActionFactory , BaseActionFactoryContext , SerializedAction , SerializedEvent , } from '../../../dynamic_actions' ; import { DrilldownState } from './drilldown_state' ; import { toastDrilldownCreated , toastDrilldownsCRUDError , insufficientLicenseLevel , invalidDrilldownType , txtDefaultTitle , toastDrilldownDeleted , toastDrilldownsDeleted , toastDrilldownEdited , } from './i18n' ; import { DrilldownTableItem } from '../components/drilldown_table' ; const helloMessageStorageKey = `drilldowns:hidWelcomeMessage` ; export interface DrilldownManagerStateDeps extends DrilldownManagerDependencies , PublicDrilldownManagerProps { } /**
 * An instance of this class holds all the state necessary for Drilldown
 * Manager. It also holds all the necessary controllers to change the state.
 *
 * `<DrilldownManager>` and other container components access this state using
 * the `useDrilldownManager()` React hook:
 *
 * ```ts
 * const state = useDrilldownManager();
 * ```
 */ export class DrilldownManagerState { /**
     * Title displayed at the top of <DrilldownManager> flyout.
     */ private readonly title$ = new BehaviorSubject < React . ReactNode > ( txtDefaultTitle ) ; /**
     * Footer displayed at the bottom of <DrilldownManager> flyout.
     */ private readonly footer$ = new BehaviorSubject < React . ReactNode > ( null ) ; /**
     * Route inside Drilldown Manager flyout that is displayed to the user. Some
     * available routes are:
     *
     * - `['create']`
     * - `['new']`
     * - `['new', 'DASHBOARD_TO_DASHBOARD_DRILLDOWN']`
     * - `['manage']`
     * - `['manage', 'yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy']`
     */ public readonly route$ : BehaviorSubject < string [ ] > ; /**
     * Whether a drilldowns welcome message should be displayed to the user at
     * the very top of the drilldowns manager flyout.
     */ public readonly hideWelcomeMessage$ : BehaviorSubject < boolean > ; /**
     * Currently selected action factory (drilldown type).
     */ public readonly actionFactory$ : BehaviorSubject < undefined | ActionFactory > ; private readonly mapEventToDrilldownItem = ( event ) : DrilldownTableItem => { const actionFactory = this . deps . actionFactories . find ( ( factory ) => factory . id === event . action . factoryId ) ; const drilldownFactoryContext = { ... this . deps . placeContext , triggers : event . triggers as string [ ] , } ; const firstTrigger = event . triggers [ 0 ] ; return { id : event . eventId , drilldownName : event . action . name , actionName : actionFactory ?. getDisplayName ( drilldownFactoryContext ) ?? event . action . factoryId , icon : actionFactory ?. getIconType ( drilldownFactoryContext ) , error : ! actionFactory ? invalidDrilldownType ( event . action . factoryId ) : ! actionFactory . isCompatibleLicense ( ) ? insufficientLicenseLevel : undefined , triggers : event . triggers . map ( ( trigger ) => this . deps . getTrigger ( trigger as string ) ) , triggerIncompatible : ! this . deps . triggers . find ( ( t ) => t === firstTrigger ) , } ; } ; public readonly events$ : BehaviorSubject < DrilldownTableItem [ ] > ; /**
     * State for each drilldown type used for new drilldown creation, so when user
     * switched between drilldown types the configuration of the previous
     * drilldown is preserved.
     */ public readonly drilldownStateByFactoryId = new Map < string , DrilldownState > ( ) ; /**
     * Whether user can unlock more drilldown types if they subscribe to a higher
     * license tier.
     */ public readonly canUnlockMoreDrilldowns : boolean ; /**
     * Used to show cloning success notification.
     */ public lastCloneRecord : null | { time : number ; templateIds : string [ ] ; } = null ; constructor ( public readonly deps ) { const hideWelcomeMessage = deps . storage . get ( helloMessageStorageKey ) ; this . hideWelcomeMessage$ = new BehaviorSubject < boolean > ( hideWelcomeMessage ?? false ) ; this . canUnlockMoreDrilldowns = deps . actionFactories . some ( ( factory ) => ! factory . isCompatibleLicense ) ; this . events$ = new BehaviorSubject < DrilldownTableItem [ ] > ( this . deps . dynamicActionManager . state . get ( ) . events . map ( this . mapEventToDrilldownItem ) ) ; deps . dynamicActionManager . state . state$ . pipe ( map ( ( state ) => state . events . map ( this . mapEventToDrilldownItem ) ) ) . subscribe ( this . events$ ) ; let { initialRoute = '' } = deps ; if ( ! initialRoute ) initialRoute = 'manage' ; else if ( initialRoute [ 0 ] === '/' ) initialRoute = initialRoute . substr ( 1 ) ; this . route$ = new BehaviorSubject ( initialRoute . split ( '/' ) ) ; this . actionFactory$ = new BehaviorSubject < undefined | ActionFactory > ( this . getActiveActionFactory ( ) ) ; this . route$ . pipe ( map ( ( ) => this . getActiveActionFactory ( ) ) ) . subscribe ( this . actionFactory$ ) ; } param title public setTitle ( title ) { this . title$ . next ( title ) ; } param footer public setFooter ( footer ) { this . footer$ . next ( footer ) ; } /**
     * Set the flyout main heading back to its default state.
     */ public resetTitle ( ) { this . setTitle ( txtDefaultTitle ) ; } /**
     * Change the screen of Drilldown Manager.
     */ public setRoute ( route ) { if ( route [ 0 ] === 'manage' ) this . deps . closeAfterCreate = false ; this . route$ . next ( route ) ; } /**
     * Callback called to hide drilldowns welcome message, and remember in local
     * storage that user opted to hide this message.
     */ public readonly hideWelcomeMessage = ( ) : void => { this . hideWelcomeMessage$ . next ( true ) ; this . deps . storage . set ( helloMessageStorageKey , true ) ; } ; /**
     * Select a different action factory.
     */ public setActionFactory ( actionFactory ) { if ( ! actionFactory ) { const route = this . route$ . getValue ( ) ; if ( route [ 0 ] === 'new' && route . length > 1 ) this . setRoute ( [ 'new' ] ) ; return ; } if ( ! this . drilldownStateByFactoryId . has ( actionFactory . id ) ) { const oldActionFactory = this . getActiveActionFactory ( ) ; const oldDrilldownState = ! ! oldActionFactory ? this . drilldownStateByFactoryId . get ( oldActionFactory . id ) : undefined ; const context = this . getActionFactoryContext ( ) ; const drilldownState = new DrilldownState ( { factory : actionFactory , placeTriggers : this . deps . triggers , placeContext : this . deps . placeContext || { } , name : this . pickName ( ! ! oldDrilldownState ? oldDrilldownState . name$ . getValue ( ) : actionFactory . getDisplayName ( this . getActionFactoryContext ( ) ) ) , triggers : [ ] , config : actionFactory . createConfig ( context ) , } ) ; this . drilldownStateByFactoryId . set ( actionFactory . id , drilldownState ) ; } this . route$ . next ( [ 'new' , actionFactory . id ] ) ; } public getActiveActionFactory ( ) { const [ step1 , id ] = this . route$ . getValue ( ) ; if ( step1 !== 'new' || ! id ) return undefined ; return this . deps . actionFactories . find ( ( factory ) => factory . id === id ) ; } /**
     * Close the drilldown flyout.
     */ public readonly close = ( ) : void => { this . deps . onClose ( ) ; } ; /**
     * Get action factory context, which also contains a custom place context
     * provided by the user who triggered rendering of the <DrilldownManager>.
     */ public getActionFactoryContext ( ) { const placeContext = this . deps . placeContext ?? [ ] ; const context = { ... placeContext , triggers : [ ] , } ; return context ; } /**
     * Get state object of the drilldown which is currently being created.
     */ public getDrilldownState ( ) { const actionFactory = this . getActiveActionFactory ( ) ; if ( ! actionFactory ) return undefined ; const drilldownState = this . drilldownStateByFactoryId . get ( actionFactory . id ) ; return drilldownState ; } /**
     * Called when user presses "Create drilldown" button to save the
     * currently edited drilldown.
     */ public async createDrilldown ( ) { const { dynamicActionManager , toastService } = this . deps ; const drilldownState = this . getDrilldownState ( ) ; if ( ! drilldownState ) return ; try { const event = drilldownState . serialize ( ) ; const triggers = drilldownState . triggers$ . getValue ( ) ; await dynamicActionManager . createEvent ( event , triggers ) ; toastService . addSuccess ( { title : toastDrilldownCreated . title ( drilldownState . name$ . getValue ( ) ) , text : toastDrilldownCreated . text , } ) ; this . drilldownStateByFactoryId . delete ( drilldownState . factory . id ) ; if ( this . deps . closeAfterCreate ) { this . deps . onClose ( ) ; } else { this . setRoute ( [ 'manage' ] ) ; } } catch ( error ) { toastService . addError ( error , { title : toastDrilldownsCRUDError , } ) ; throw error ; } } param ids public readonly onDelete = ( ids ) => { ( async ( ) => { const { dynamicActionManager , toastService } = this . deps ; try { await dynamicActionManager . deleteEvents ( ids ) ; this . deps . toastService . addSuccess ( ids . length === 1 ? { title : toastDrilldownDeleted . title , text : toastDrilldownDeleted . text , } : { title : toastDrilldownsDeleted . title ( ids . length ) , text : toastDrilldownsDeleted . text , } ) ; } catch ( error ) { toastService . addError ( error , { title : toastDrilldownsCRUDError , } ) ; } } ) ( ) . catch ( console . error ) ; } ; /**
     * Clone a list of selected templates.
     */ public readonly onClone = async ( templateIds ) => { const { templates } = this . deps ; if ( ! templates ) return ; const templatesToClone = templateIds . map ( ( templateId ) => templates . find ( ( { id } ) => id === templateId ) ) . filter ( Boolean ) as DrilldownTemplate [ ] ; for ( const template of templatesToClone ) { await this . cloneTemplate ( template ) ; } this . lastCloneRecord = { time : Date . now ( ) , templateIds , } ; this . setRoute ( [ 'manage' ] ) ; } ; private async cloneTemplate ( template ) { const { dynamicActionManager } = this . deps ; const name = this . pickName ( template . name ) ; const action = { factoryId : template . factoryId , name , config : ( template . config || { } ) as SerializableRecord , } ; await dynamicActionManager . createEvent ( action , template . triggers ) ; } /**
     * Checks if drilldown with such a name already exists.
     */ private hasDrilldownWithName ( name ) { const { events } = this . deps . dynamicActionManager . state . get ( ) ; for ( const event of events ) if ( event . action . name === name ) return true ; return false ; } /**
     * Picks a unique name for the cloned drilldown. Adds "(copy)", "(copy 1)",
     * "(copy 2)", etc. if drilldown with such name already exists.
     */ private pickName ( name ) { if ( this . hasDrilldownWithName ( name ) ) { const matches = name . match ( /(.*) (\(copy[^\)]*\))/ ) ; if ( matches ) name = matches [ 1 ] ; for ( let i = 0 ; i < 100 ; i ++ ) { const proposedName = ! i ? `${ name } (copy)` : `${ name } (copy ${ i })` ; const exists = this . hasDrilldownWithName ( proposedName ) ; if ( ! exists ) return proposedName ; } } return name ; } public readonly onCreateFromTemplate = async ( templateId ) => { const { templates } = this . deps ; if ( ! templates ) return ; const template = templates . find ( ( { id } ) => id === templateId ) ; if ( ! template ) return ; const actionFactory = this . deps . actionFactories . find ( ( { id } ) => id === template . factoryId ) ; if ( ! actionFactory ) return ; this . setActionFactory ( actionFactory ) ; const drilldownState = this . getDrilldownState ( ) ; if ( drilldownState ) { drilldownState . setName ( this . pickName ( template . name ) ) ; drilldownState . setTriggers ( template . triggers ) ; drilldownState . setConfig ( template . config as SerializableRecord ) ; } } ; public readonly onCreateFromDrilldown = async ( eventId ) => { const { dynamicActionManager } = this . deps ; const { events } = dynamicActionManager . state . get ( ) ; const event = events . find ( ( ev ) => ev . eventId === eventId ) ; if ( ! event ) return ; const actionFactory = this . deps . actionFactories . find ( ( { id } ) => id === event . action . factoryId ) ; if ( ! actionFactory ) return ; this . setActionFactory ( actionFactory ) ; const drilldownState = this . getDrilldownState ( ) ; if ( drilldownState ) { drilldownState . setName ( this . pickName ( event . action . name ) ) ; drilldownState . setTriggers ( event . triggers ) ; drilldownState . setConfig ( event . action . config ) ; } } ; param eventId public createEventDrilldownState ( eventId ) { const { dynamicActionManager , actionFactories , triggers : placeTriggers } = this . deps ; const { events } = dynamicActionManager . state . get ( ) ; const event = events . find ( ( ev ) => ev . eventId === eventId ) ; if ( ! event ) return null ; const factory = actionFactories . find ( ( { id } ) => id === event . action . factoryId ) ; if ( ! factory ) return null ; const { action , triggers } = event ; const { name , config } = action ; const state = new DrilldownState ( { factory , placeContext : this . getActionFactoryContext ( ) , placeTriggers , name , config , triggers , } ) ; return state ; } param eventId param drilldownState public async updateEvent ( eventId , drilldownState ) { const { dynamicActionManager , toastService } = this . deps ; const action = drilldownState . serialize ( ) ; try { await dynamicActionManager . updateEvent ( eventId , action , drilldownState . triggers$ . getValue ( ) ) ; toastService . addSuccess ( { title : toastDrilldownEdited . title ( action . name ) , text : toastDrilldownEdited . text , } ) ; this . setRoute ( [ 'manage' ] ) ; } catch ( error ) { toastService . addError ( error , { title : toastDrilldownsCRUDError , } ) ; throw error ; } } public readonly useTitle = ( ) => useObservable ( this . title$ , this . title$ . getValue ( ) ) ; public readonly useFooter = ( ) => useObservable ( this . footer$ , this . footer$ . getValue ( ) ) ; public readonly useRoute = ( ) => useObservable ( this . route$ , this . route$ . getValue ( ) ) ; public readonly useWelcomeMessage = ( ) => useObservable ( this . hideWelcomeMessage$ , this . hideWelcomeMessage$ . getValue ( ) ) ; public readonly useActionFactory = ( ) => useObservable ( this . actionFactory$ , this . actionFactory$ . getValue ( ) ) ; public readonly useEvents = ( ) => useObservable ( this . events$ , this . events$ . getValue ( ) ) ; }