"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.DynamicActionManager = void 0; var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); var _uuid = require("uuid"); var _common = require("@kbn/kibana-utils-plugin/common"); var _dynamic_action_manager_state = require("./dynamic_action_manager_state"); var _dynamic_action_grouping = require("./dynamic_action_grouping"); /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License * 2.0 and the Server Side Public License, v 1; you may not use this file except * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ 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; }; class DynamicActionManager { /** * UI State of the dynamic action manager. */ constructor(params) { (0, _defineProperty2.default)(this, "idPrefix", `D_ACTION_${DynamicActionManager.idPrefixCounter++}_`); (0, _defineProperty2.default)(this, "stopped", false); (0, _defineProperty2.default)(this, "reloadSubscription", void 0); (0, _defineProperty2.default)(this, "ui", (0, _common.createStateContainer)(_dynamic_action_manager_state.defaultState, _dynamic_action_manager_state.transitions, _dynamic_action_manager_state.selectors)); (0, _defineProperty2.default)(this, "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. */ (0, _defineProperty2.default)(this, "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 => { /* eslint-disable no-console */ console.log('Dynamic action manager storage reload failed.'); console.error(error); /* eslint-enable */ }); }); // Public API: --------------------------------------------------------------- /** * Read-only state container of dynamic action manager. Use it to perform all * *read* operations. */ (0, _defineProperty2.default)(this, "state", this.ui); this.params = params; } 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. */ generateActionId(eventId) { return this.idPrefix + eventId; } reviveAction(event) { const { eventId, triggers, action } = event; const { uiActions, isCompatible } = this.params; const actionId = this.generateActionId(eventId); if (!uiActions.hasActionFactory(action.factoryId)) { // eslint-disable-next-line no-console 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); uiActions.registerAction({ ...actionDefinition, id: actionId, grouping: _dynamic_action_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, actionId); } } 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); } /** * 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. */ 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. */ 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(); } } /** * Creates a new event. * * 1. Stores event in @type {DynamicActionStorage} storage. * 2. Optimistically adds it to UI state, and rolls back on failure. * 3. Adds action to `ui_actions` registry. * * @param action Dynamic action for which to create an event. * @param triggers List of triggers to which action should react. */ async createEvent(action, triggers) { if (!triggers.length) { // This error should never happen, hence it is not translated. throw new Error('No triggers selected for event.'); } const event = { eventId: (0, _uuid.v4)(), 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; } } /** * Updates an existing event. Fails if event with given `eventId` does not * exit. * * 1. Updates the event in @type {DynamicActionStorage} storage. * 2. Optimistically replaces the old event by the new one in UI state, and * rolls back on failure. * 3. Replaces action in `ui_actions` registry with the new event. * * * @param eventId ID of the event to replace. * @param action New action for which to create the event. * @param triggers List of triggers to which action should react. */ 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; } } /** * Removes existing event. Throws if event does not exist. * * 1. Removes the event from @type {DynamicActionStorage} storage. * 2. Optimistically removes event from UI state, and puts it back on failure. * 3. Removes associated action from `ui_actions` registry. * * @param eventId ID of the event to remove. */ 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; } } /** * Deletes multiple events at once. * * @param eventIds List of event IDs. */ async deleteEvents(eventIds) { await Promise.all(eventIds.map(this.deleteEvent.bind(this))); } } exports.DynamicActionManager = DynamicActionManager; (0, _defineProperty2.default)(DynamicActionManager, "idPrefixCounter", 0);