/* * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 */ import React from 'react'; import { DetectorsService } from '../services'; import { NotificationsStart } from 'opensearch-dashboards/public'; import { CreateDetectorState } from '../pages/CreateDetector/containers/CreateDetector'; import { ICalloutProps, resolveType, TCalloutColor } from '../pages/Main/components/Callout'; import { CreateDetectorResponse, ISavedObjectsService, ServerResponse } from '../../types'; import { CreateMappingsResponse } from '../../server/models/interfaces'; import { logTypesWithDashboards, ROUTES } from '../utils/constants'; import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { Toast } from '@opensearch-project/oui/src/eui_components/toast/global_toast_list'; import { RouteComponentProps } from 'react-router-dom'; import { DataStore } from './DataStore'; import { v4 as uuidv4 } from 'uuid'; export interface IDetectorsStore { readonly service: DetectorsService; readonly notifications: NotificationsStart; readonly savedObjectsService: ISavedObjectsService; readonly history: RouteComponentProps['history'] | undefined; setState: (state: IDetectorsState, history: RouteComponentProps['history']) => void; getState: () => IDetectorsState | undefined; deleteState: () => void; clearNotifications: () => void; resolvePendingCreationRequest: () => Promise<{ detectorId?: string; dashboardId?: string; ok: boolean; }>; setHandlers: ( calloutHandler: (callout?: ICalloutProps) => void, toastHandler: (toasts?: Toast[]) => void ) => void; } export interface IDetectorsState { pendingRequests: Promise[]; detectorInput: CreateDetectorState; } /** * Class is used to make detector's API calls and cache the detectors. * If there is a cache data requests are skipped and result is returned from the cache. * If cache is invalidated then the request is triggered to get a new set of data. * * @class DetectorsStore * @implements IDetectorsStore * @param {BrowserServices} services Uses services to make API requests */ export class DetectorsStore implements IDetectorsStore { /** * Rule service instance * * @property {DetectorsService} service * @readonly */ readonly service: DetectorsService; /** * Notifications * @property {NotificationsStart} * @readonly */ readonly notifications: NotificationsStart; /** * SavedObjectsService * @property {ISavedObjectsService} * @readonly */ readonly savedObjectsService: ISavedObjectsService; /** * Router history * @property {RouteComponentProps['history']} * @readonly */ history: RouteComponentProps['history'] | undefined = undefined; /** * Store state * @private {IDetectorsState} */ private state: IDetectorsState | undefined; /** * List of all shown toasts * @private */ private toasts: Toast[] = []; constructor( service: DetectorsService, notifications: NotificationsStart, savedObjectsService: ISavedObjectsService ) { this.service = service; this.notifications = notifications; this.savedObjectsService = savedObjectsService; } public setState = (state: IDetectorsState, history: RouteComponentProps['history']): void => { this.state = state; this.history = history; this.showNotification('Attempting to create the detector.', undefined, 'primary', true); }; public getState = (): IDetectorsState | undefined => (this.state ? this.state : undefined); public deleteState = (): void => { delete this.state; }; public clearNotifications = (): void => { this.hideCallout(); this.toasts = []; this.showToastCallback([]); }; private showNotification = ( title: string, message?: string, type?: TCalloutColor, loading?: boolean, btnText?: string, btnHandler?: (e: any) => void ): void => { if (!type) type = 'primary'; const closeAllToasts = () => { this.toasts = []; this.showToastCallback(this.toasts); }; const btn = btnText && ( { btnHandler && btnHandler(e); this.hideCallout(); closeAllToasts(); }} size="s" > {btnText} ); const messageBody = ( {message && {message}} {btn} ); this.showCalloutCallback({ type, title, message: messageBody, closeHandler: this.hideCallout, }); const { color, iconType } = resolveType(type); this.toasts.push({ title, color, iconType, id: `toastsKey_${uuidv4()}`, text: messageBody, }); this.showToastCallback(this.toasts); }; private viewDetectorConfiguration = (): void => { const state = DataStore.detectors.getState(); const detectorInput = { ...state?.detectorInput }; DataStore.detectors.deleteState(); this.history?.push({ pathname: `${ROUTES.DETECTORS_CREATE}`, state: { detectorInput }, }); }; public resolvePendingCreationRequest = async (): Promise<{ detectorId?: string; dashboardId?: string; ok: boolean; }> => { if (this.state?.pendingRequests) { const [mappingsResponse, detectorResponse] = (await Promise.all( this.state?.pendingRequests )) as [ServerResponse, ServerResponse]; let title: string = `Create detector failed.`; if (!mappingsResponse.ok) { const message = 'Double check the field mappings and try again.'; this.showNotification( title, message, 'danger', false, 'Review detector configuration', DataStore.detectors.viewDetectorConfiguration ); return Promise.resolve({ ok: false, error: { title, message }, }); } if (!detectorResponse.ok) { this.showNotification( title, detectorResponse.error, 'danger', false, 'Review detector configuration', DataStore.detectors.viewDetectorConfiguration ); return Promise.resolve({ ok: false, error: { title, message: detectorResponse.error, }, }); } let dashboardId; const detector = detectorResponse.response.detector; const detectorId = detectorResponse.response._id; if (logTypesWithDashboards.has(detector.detector_type)) { const dashboardResponse = await this.createDashboard( detector.name, detector.detector_type, detectorId, detector.inputs[0].detector_input.indices ); if (dashboardResponse && dashboardResponse.ok) { dashboardId = dashboardResponse.response.id; } else { const dashboard = await this.savedObjectsService.getDashboard(detectorId); if (dashboard) { dashboardId = dashboard.id; } } } const goToDetectorDetails = (e: any) => { e.preventDefault(); DataStore.detectors.deleteState(); this.history?.push(`${ROUTES.DETECTOR_DETAILS}/${detectorId}`); }; title = `Detector created successfully: ${detectorResponse.response.detector.name}`; this.showNotification( title, undefined, 'success', false, 'View detector', goToDetectorDetails ); return Promise.resolve({ detectorId: detectorId, dashboardId: dashboardId, ok: true, }); } return Promise.resolve({ ok: false }); }; private createDashboard = ( detectorName: string, logType: string, detectorId: string, inputIndices: string[] ) => { return this.savedObjectsService .createSavedObject(detectorName, logType, detectorId, inputIndices) .catch((error: any) => { console.error(error); }); }; /** * A handler function that store gets from the Main component to show/hide the callout message * @param {ICalloutProps | undefined} callout */ private showCalloutCallback = (callout?: ICalloutProps | undefined): void => {}; private hideCallout = (): void => this.showCalloutCallback(undefined); /** * A handler function that store gets from the Main component to show/hide the toast message * @param {Toast[] | undefined} toasts */ private showToastCallback = (toasts?: Toast[] | undefined): void => {}; public hideToast = (removedToast: any): void => { this.toasts = this.toasts.filter((toast: Toast) => toast.id !== removedToast.id); this.showToastCallback(this.toasts); }; public setHandlers = ( calloutHandler: (callout?: ICalloutProps) => void, toastHandler: (toasts?: Toast[]) => void ): void => { this.showCalloutCallback = calloutHandler; this.showToastCallback = toastHandler; }; }