"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.apiService = exports.ApiService = void 0; var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); var _rxjs = require("rxjs"); var _common = require("../../common"); var _helpers = require("./helpers"); var _config = require("./config.service"); /* * 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. */ class ApiService { constructor() { (0, _defineProperty2.default)(this, "_isEnabled", false); (0, _defineProperty2.default)(this, "client", void 0); (0, _defineProperty2.default)(this, "pluginState$", void 0); (0, _defineProperty2.default)(this, "isLoading$", new _rxjs.BehaviorSubject(false)); (0, _defineProperty2.default)(this, "isGuidePanelOpen$", new _rxjs.BehaviorSubject(false)); (0, _defineProperty2.default)(this, "configService", new _config.ConfigService()); } setup(httpClient, isEnabled) { this._isEnabled = isEnabled; this.client = httpClient; this.pluginState$ = new _rxjs.BehaviorSubject(undefined); this.isGuidePanelOpen$ = new _rxjs.BehaviorSubject(false); this.isLoading$ = new _rxjs.BehaviorSubject(false); this.configService.setup(httpClient); } createGetPluginStateObservable() { return new _rxjs.Observable(observer => { const controller = new AbortController(); const signal = controller.signal; this.isLoading$.next(true); this.client.get(`${_common.API_BASE_PATH}/state`, { signal }).then(({ pluginState }) => { this.isLoading$.next(false); observer.next(pluginState); this.pluginState$.next(pluginState); observer.complete(); }).catch(error => { this.isLoading$.next(false); // if the request fails, we initialize the state with error observer.next({ status: 'error', isActivePeriod: false }); this.pluginState$.next({ status: 'error', isActivePeriod: false }); observer.complete(); }); return () => { this.isLoading$.next(false); controller.abort(); }; }); } /** * An Observable with the plugin state. * Initially the state is fetched from the backend. * Subsequently, the observable is updated automatically, when the state changes. */ fetchPluginState$() { if (!this._isEnabled) { return (0, _rxjs.of)(undefined); } if (!this.client) { throw new Error('ApiService has not be initialized.'); } const currentState = this.pluginState$.value; // if currentState is undefined, it was not fetched from the backend yet // or the request was cancelled or failed // also check if we don't have a request in flight already if (!currentState && !this.isLoading$.value) { this.isLoading$.next(true); return (0, _rxjs.concat)(this.createGetPluginStateObservable(), this.pluginState$); } return this.pluginState$; } /** * Async operation to fetch state for all guides. * This is useful for the onboarding landing page, * where all guides are displayed with their corresponding status. */ async fetchAllGuidesState() { if (!this._isEnabled) { return undefined; } if (!this.client) { throw new Error('ApiService has not be initialized.'); } try { return await this.client.get(`${_common.API_BASE_PATH}/guides`); } catch (error) { throw error; } } /** * Updates the SO with the updated plugin state and refreshes the observables. * This is largely used internally and for tests. * @param {{status?: PluginStatus; guide?: GuideState}} state the updated plugin state * @param {boolean} panelState boolean to determine whether the dropdown panel should open or not * @return {Promise} a promise with the updated plugin state or undefined */ async updatePluginState(state, panelState) { if (!this._isEnabled) { return undefined; } if (!this.client) { throw new Error('ApiService has not be initialized.'); } try { this.isLoading$.next(true); const response = await this.client.put(`${_common.API_BASE_PATH}/state`, { body: JSON.stringify(state) }); this.isLoading$.next(false); // update the guide state in the plugin state observable this.pluginState$.next(response.pluginState); this.isGuidePanelOpen$.next(panelState); return response; } catch (error) { this.isLoading$.next(false); throw error; } } /** * Activates a guide by guideId. * This is useful for the onboarding landing page, when a user selects a guide to start or continue. * @param {GuideId} guideId the id of the guide (one of search, kubernetes, siem) * @param {GuideState} guide (optional) the selected guide state, if it exists (i.e., if a user is continuing a guide) * @return {Promise} a promise with the updated plugin state */ async activateGuide(guideId, guide) { // If we already have the guide state (i.e., user has already started the guide at some point), // simply pass it through so they can continue where they left off, and update the guide to active if (guide) { return await this.updatePluginState({ status: 'in_progress', guide: { ...guide, isActive: true } }, true); } // If this is the 1st-time attempt, we need to create the default state const guideConfig = await this.configService.getGuideConfig(guideId); if (guideConfig) { const updatedSteps = guideConfig.steps.map((step, stepIndex) => { const isFirstStep = stepIndex === 0; return { id: step.id, // Only the first step should be activated when activating a new guide status: isFirstStep ? 'active' : 'inactive' }; }); const updatedGuide = { guideId, isActive: true, status: 'not_started', steps: updatedSteps }; return await this.updatePluginState({ status: 'in_progress', guide: updatedGuide }, true); } } /** * Marks a guide as inactive. * This is useful for the dropdown panel, when a user quits a guide. * @param {GuideState} guide the selected guide state * @return {Promise} a promise with the updated plugin state */ async deactivateGuide(guide) { return await this.updatePluginState({ status: 'quit', guide: { ...guide, isActive: false } }, false); } /** * Completes a guide. * Updates the overall guide status to 'complete', and marks it as inactive. * This is useful for the dropdown panel, when the user clicks the "Continue using Elastic" button after completing all steps. * @param {GuideId} guideId the id of the guide (one of search, kubernetes, siem) * @return {Promise} a promise with the updated plugin state */ async completeGuide(guideId) { const pluginState = await (0, _rxjs.firstValueFrom)(this.fetchPluginState$()); // For now, returning undefined if consumer attempts to complete a guide that is not active if (!(0, _helpers.isGuideActive)(pluginState, guideId)) return undefined; const { activeGuide } = pluginState; // All steps should be complete at this point // However, we do a final check here as a safeguard const allStepsComplete = Boolean(activeGuide.steps.find(step => step.status === 'complete')); if (allStepsComplete) { const updatedGuide = { ...activeGuide, isActive: false, status: 'complete' }; return await this.updatePluginState({ status: 'complete', guide: updatedGuide }, false); } } /** * An observable with the boolean value if the step is in progress (i.e., user clicked "Start" on a step). * Returns true, if the passed params identify the guide step that is currently in progress. * Returns false otherwise. * @param {GuideId} guideId the id of the guide (one of search, kubernetes, siem) * @param {GuideStepIds} stepId the id of the step in the guide * @return {Observable} an observable with the boolean value */ isGuideStepActive$(guideId, stepId) { return this.fetchPluginState$().pipe((0, _rxjs.map)(pluginState => { if (!(0, _helpers.isGuideActive)(pluginState, guideId)) return false; return (0, _helpers.isStepInProgress)(pluginState.activeGuide, guideId, stepId); })); } /** * An observable with the boolean value if the step is ready_to_complete (i.e., user needs to click the "Mark done" button). * Returns true, if the passed params identify the guide step that is currently ready_to_complete. * Returns false otherwise. * @param {GuideId} guideId the id of the guide (one of search, kubernetes, siem) * @param {GuideStepIds} stepId the id of the step in the guide * @return {Observable} an observable with the boolean value */ isGuideStepReadyToComplete$(guideId, stepId) { return this.fetchPluginState$().pipe((0, _rxjs.map)(pluginState => { if (!(0, _helpers.isGuideActive)(pluginState, guideId)) return false; return (0, _helpers.isStepReadyToComplete)(pluginState.activeGuide, guideId, stepId); })); } /** * Updates the selected step to 'in_progress' state. * This is useful for the dropdown panel, when the user clicks the "Start" button for the active step. * @param {GuideId} guideId the id of the guide (one of search, kubernetes, siem) * @param {GuideStepIds} stepId the id of the step * @return {Promise} a promise with the updated plugin state */ async startGuideStep(guideId, stepId) { const pluginState = await (0, _rxjs.firstValueFrom)(this.fetchPluginState$()); // For now, returning undefined if consumer attempts to start a step for a guide that isn't active if (!(0, _helpers.isGuideActive)(pluginState, guideId)) { return undefined; } const { activeGuide } = pluginState; const updatedSteps = activeGuide.steps.map(step => { // Mark the current step as in_progress if (step.id === stepId) { return { id: step.id, status: 'in_progress' }; } // All other steps return as-is return step; }); const currentGuide = { guideId, isActive: true, status: 'in_progress', steps: updatedSteps }; return await this.updatePluginState({ guide: currentGuide }, false); } /** * Completes the guide step identified by the passed params. * A noop if the passed step is not active. * @param {GuideId} guideId the id of the guide (one of search, kubernetes, siem) * @param {GuideStepIds} stepId the id of the step in the guide * @return {Promise} a promise with the updated state or undefined if the operation fails */ async completeGuideStep(guideId, stepId, params) { const pluginState = await (0, _rxjs.firstValueFrom)(this.fetchPluginState$()); // For now, returning undefined if consumer attempts to complete a step for a guide that isn't active if (!(0, _helpers.isGuideActive)(pluginState, guideId)) { return undefined; } const { activeGuide } = pluginState; const isCurrentStepInProgress = (0, _helpers.isStepInProgress)(activeGuide, guideId, stepId); const isCurrentStepReadyToComplete = (0, _helpers.isStepReadyToComplete)(activeGuide, guideId, stepId); const guideConfig = await this.configService.getGuideConfig(guideId); const stepConfig = (0, _helpers.getStepConfig)(guideConfig, activeGuide.guideId, stepId); const isManualCompletion = stepConfig ? !!stepConfig.manualCompletion : false; const isLastStepInGuide = (0, _helpers.isLastStep)(guideConfig, guideId, stepId); if (isCurrentStepInProgress || isCurrentStepReadyToComplete) { const updatedSteps = (0, _helpers.getCompletedSteps)(activeGuide, stepId, // if current step is in progress and configured for manual completion, // set the status to ready_to_complete isManualCompletion && isCurrentStepInProgress); const status = await this.configService.getGuideStatusOnStepCompletion({ isLastStepInGuide, isManualCompletion, isStepReadyToComplete: isCurrentStepReadyToComplete }); const currentGuide = { guideId, isActive: true, status, steps: updatedSteps, params }; return await this.updatePluginState({ guide: currentGuide }, // the panel is opened when the step is being set to complete. // that happens when the step is not configured for manual completion // or it's already ready_to_complete !isManualCompletion || isCurrentStepReadyToComplete); } return undefined; } /** * An observable with the boolean value if the guided onboarding is currently active for the integration. * Returns true, if the passed integration is used in the current guide's step. * Returns false otherwise. * @param {string} integration the integration (package name) to check for in the guided onboarding config * @return {Observable} an observable with the boolean value */ isGuidedOnboardingActiveForIntegration$(integration) { return this.fetchPluginState$().pipe((0, _rxjs.concatMap)(state => (0, _rxjs.from)(this.configService.isIntegrationInGuideStep(state === null || state === void 0 ? void 0 : state.activeGuide, integration)))); } /** * Completes the guide step identified by the integration. * A noop if the active step is not configured with the passed integration. * @param {GuideId} integration the integration (package name) that identifies the active guide step * @return {Promise} a promise with the updated state or undefined if the operation fails */ async completeGuidedOnboardingForIntegration(integration) { if (!integration) return undefined; const pluginState = await (0, _rxjs.firstValueFrom)(this.fetchPluginState$()); if (!(0, _helpers.isGuideActive)(pluginState)) return undefined; const { activeGuide } = pluginState; const inProgressStepId = (0, _helpers.getInProgressStepId)(activeGuide); if (!inProgressStepId) return undefined; const isIntegrationStepActive = await this.configService.isIntegrationInGuideStep(activeGuide, integration); if (isIntegrationStepActive) { return await this.completeGuideStep(activeGuide.guideId, inProgressStepId); } } /** * Sets the plugin state to "skipped". * This is used on the landing page when the user clicks the button to skip the guided setup. * @return {Promise} a promise with the updated state or undefined if the operation fails */ async skipGuidedOnboarding() { return await this.updatePluginState({ status: 'skipped' }, false); } /** * Gets the config for the guide. * @return {Promise} a promise with the guide config or undefined if the config is not found */ async getGuideConfig(guideId) { if (!this._isEnabled) { return undefined; } if (!this.client) { throw new Error('ApiService has not be initialized.'); } this.isLoading$.next(true); const config = await this.configService.getGuideConfig(guideId); this.isLoading$.next(false); return config; } get isEnabled() { return this._isEnabled; } } exports.ApiService = ApiService; const apiService = new ApiService(); exports.apiService = apiService;