diff --git a/package.json b/package.json index d09c1a22..d20e006a 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "babel-polyfill": "^6.26.0", "eslint-plugin-no-unsanitized": "^3.0.2", "eslint-plugin-prefer-object-spread": "^1.2.1", - "jest-canvas-mock": "^2.5.2", + "jest-canvas-mock": "^2.5.1", "lint-staged": "^9.2.0", "moment": "^2.24.0", "redux-mock-store": "^1.5.4", diff --git a/public/pages/DefineDetector/components/CustomResultIndex/CustomResultIndex.tsx b/public/pages/DefineDetector/components/CustomResultIndex/CustomResultIndex.tsx index 55e53297..ccbc08e5 100644 --- a/public/pages/DefineDetector/components/CustomResultIndex/CustomResultIndex.tsx +++ b/public/pages/DefineDetector/components/CustomResultIndex/CustomResultIndex.tsx @@ -89,7 +89,7 @@ function CustomResultIndex(props: CustomResultIndexProps) { { const isCatIndicesRequesting = useSelector( (state: AppState) => state.opensearch.requesting ) as boolean; + const visibleAliases = useSelector( + (state: AppState) => state.opensearch.aliases + ) as IndexAlias[]; /* Determine if the result index is missing based on several conditions: - If the detector is still loading, the result index is not missing. - If the result index retrieved from the detector is empty, it is not missing. - If cat indices are being requested, the result index is not missing. - - If visible indices are empty, it is likely there is an issue retrieving existing indices. + - If visible indices/aliaes are empty, it is likely there is an issue retrieving existing indices. To be safe, we'd rather not show the error message and consider the result index not missing. - If the result index is not found in the visible indices, then it is missing. */ + const resultIndexOrAlias = get(detector, 'resultIndex', '') const isResultIndexMissing = isLoadingDetector ? false : isEmpty(get(detector, 'resultIndex', '')) ? false : isCatIndicesRequesting ? false - : isEmpty(visibleIndices) + : isEmpty(visibleIndices) || isEmpty(visibleAliases) ? false - : !containsIndex(get(detector, 'resultIndex', ''), visibleIndices); + : !containsIndex(resultIndexOrAlias, visibleIndices) && !containsAlias(resultIndexOrAlias, visibleAliases); // debug message: prints visibleIndices if isResultIndexMissing is true if (isResultIndexMissing) { - console.log(`isResultIndexMissing is true, visibleIndices: ${visibleIndices}, detector result index: ${get(detector, 'resultIndex', '')}`); + // The JSON.stringify method converts a JavaScript object or value to a JSON string. The optional null parameter is for the replacer function (not used here), and 2 specifies the indentation level for pretty-printing the JSON. + console.log(`isResultIndexMissing is true, visibleIndices: ${JSON.stringify(visibleIndices, null, 2)}, visibleAliases: ${JSON.stringify(visibleAliases, null, 2)}, detector result index: ${resultIndexOrAlias}`); } // String to set in the modal if the realtime detector and/or historical analysis @@ -197,20 +202,21 @@ export const DetectorDetail = (props: DetectorDetailProps) => { scroll(0, 0); }, []); - // Getting all visible indices. Will re-fetch if changes to the detector (e.g., + // Getting all visible indices & aliases. Will re-fetch if changes to the detector (e.g., // detector starts, result index recreated or user switches tabs to re-fetch detector) useEffect(() => { - const getInitialIndices = async () => { + const getInitialIndicesAliases = async () => { try { await dispatch(getIndices('', dataSourceId)); + await dispatch(getAliases('', dataSourceId)); } catch (error) { console.error(error); - core.notifications.toasts.addDanger('Error getting all indices'); + core.notifications.toasts.addDanger('Error getting all indices or aliases'); } }; // only need to check if indices exist after detector finishes loading if (!isLoadingDetector) { - getInitialIndices(); + getInitialIndicesAliases(); } }, [detector]); diff --git a/public/pages/DetectorDetail/containers/__tests__/CustomIndexErrorMsg.test.tsx b/public/pages/DetectorDetail/containers/__tests__/CustomIndexErrorMsg.test.tsx index 3848fa2c..93abd62f 100644 --- a/public/pages/DetectorDetail/containers/__tests__/CustomIndexErrorMsg.test.tsx +++ b/public/pages/DetectorDetail/containers/__tests__/CustomIndexErrorMsg.test.tsx @@ -209,7 +209,7 @@ describe('detector detail', () => { expect(element).toBeNull(); }); - test('the result index is not found in the visible indices', () => { + test('the result index is not found in the visible indices but alias is empty', () => { const detectorInfo = { detector: getRandomDetector(true, resultIndex), hasError: false, @@ -236,7 +236,7 @@ describe('detector detail', () => { const element = screen.queryByTestId('missingResultIndexCallOut'); // Assert that the element is in the document - expect(element).not.toBeNull(); + expect(element).toBeNull(); }); test('the result index is found in the visible indices', () => { @@ -272,4 +272,81 @@ describe('detector detail', () => { // Assert that the element is not in the document expect(element).toBeNull(); }); + + test('the result index prefix is found in the visible aliaes', () => { + const detector = getRandomDetector(true, resultIndex); + const resultIndexFull = resultIndex + '-history-2024.06.05-1'; + + // Set up the mock implementation for useFetchDetectorInfo + (useFetchDetectorInfo as jest.Mock).mockImplementation(() => ({ + detector: detector, + hasError: false, + isLoadingDetector: false, + errorMessage: undefined, + })); + + const initialState = { + opensearch: { + indices: [ + { health: 'green', index: '.kibana_-962704462_v992471_1' }, + { health: 'green', index: resultIndexFull}, + ], + aliases : [ + {index: '.opendistro-anomaly-results-history-2024.06.08-1', alias: '.opendistro-anomaly-results'}, + {index: resultIndexFull, alias: resultIndex}, + {index: '.kibana_1', alias: '.kibana'}, + ], + requesting: false, + }, + ad: { + detectors: {}, + }, + alerting: { + monitors: {}, + }, + }; + + renderWithRouter(detectorId, initialState); + const element = screen.queryByTestId('missingResultIndexCallOut'); + + // Assert that the element is not in the document + expect(element).toBeNull(); + }); + + test('the result index prefix is not found in both visible aliaes and indices', () => { + const detector = getRandomDetector(true, resultIndex); + + // Set up the mock implementation for useFetchDetectorInfo + (useFetchDetectorInfo as jest.Mock).mockImplementation(() => ({ + detector: detector, + hasError: false, + isLoadingDetector: false, + errorMessage: undefined, + })); + + const initialState = { + opensearch: { + indices: [ + { health: 'green', index: '.kibana_-962704462_v992471_1' }, + ], + aliases : [ + {index: '.opendistro-anomaly-results-history-2024.06.08-1', alias: '.opendistro-anomaly-results'}, + {index: '.kibana_1', alias: '.kibana'}, + ], + requesting: false, + }, + ad: { + detectors: {}, + }, + alerting: { + monitors: {}, + }, + }; + + renderWithRouter(detectorId, initialState); + const element = screen.queryByTestId('missingResultIndexCallOut'); + + // Assert that the element is not in the document + expect(element).not.toBeNull(); + }); }); diff --git a/public/pages/DetectorDetail/utils/helpers.tsx b/public/pages/DetectorDetail/utils/helpers.tsx index bb57e319..8eb0e7a2 100644 --- a/public/pages/DetectorDetail/utils/helpers.tsx +++ b/public/pages/DetectorDetail/utils/helpers.tsx @@ -17,7 +17,7 @@ import { DETECTOR_STATE } from '../../../../server/utils/constants'; import { Detector } from '../../../models/interfaces'; import { EuiHealth } from '@elastic/eui'; import moment from 'moment'; -import { CatIndex } from '../../../../server/models/types'; +import { CatIndex, IndexAlias } from '../../../../server/models/types'; export const getInitFailureMessageAndActionItem = (error: string): object => { const failureDetails = Object.values(DETECTOR_INIT_FAILURES); @@ -142,6 +142,17 @@ export const getDetectorStateDetails = ( ); }; +/** + * Checks if any of the given indices contain the specified index. + * + * This function iterates through an array of `CatIndex` objects and checks if the `index` property of any + * `CatIndex` object equals to the specified `index` string. It returns `true` if such an `index` is found, + * otherwise it returns `false`. + * + * @param index - The string to check against the `index` properties of the `CatIndex` objects. + * @param indices - An array of `CatIndex` objects to search through. + * @returns A boolean value indicating whether any `CatIndex` object's `index` property equals to the specified prefix. + */ export const containsIndex = (index: string, indices: CatIndex[]) => { let containsIndex = false; if (!isEmpty(indices)) { @@ -153,3 +164,15 @@ export const containsIndex = (index: string, indices: CatIndex[]) => { } return containsIndex; }; + +export const containsAlias = (alias: string, aliases: IndexAlias[]) => { + let containsAlias = false; + if (!isEmpty(aliases)) { + aliases.forEach((catAlias: IndexAlias) => { + if (get(catAlias, 'alias', '') == alias) { + containsAlias = true; + } + }); + } + return containsAlias; +}; diff --git a/public/pages/ReviewAndCreate/components/DetectorDefinitionFields/DetectorDefinitionFields.tsx b/public/pages/ReviewAndCreate/components/DetectorDefinitionFields/DetectorDefinitionFields.tsx index ccdc50e5..e67f33a0 100644 --- a/public/pages/ReviewAndCreate/components/DetectorDefinitionFields/DetectorDefinitionFields.tsx +++ b/public/pages/ReviewAndCreate/components/DetectorDefinitionFields/DetectorDefinitionFields.tsx @@ -131,6 +131,10 @@ export const DetectorDefinitionFields = ( } }; + const minAge = get(props, 'detector.resultIndexMinAge', '-'); + const minSize = get(props, 'detector.resultIndexMinSize', '-'); + const ttl = get(props, 'detector.resultIndexTtl', '-'); + return ( diff --git a/public/pages/ReviewAndCreate/containers/__tests__/__snapshots__/ReviewAndCreate.test.tsx.snap b/public/pages/ReviewAndCreate/containers/__tests__/__snapshots__/ReviewAndCreate.test.tsx.snap index f257fde9..b723fc9b 100644 --- a/public/pages/ReviewAndCreate/containers/__tests__/__snapshots__/ReviewAndCreate.test.tsx.snap +++ b/public/pages/ReviewAndCreate/containers/__tests__/__snapshots__/ReviewAndCreate.test.tsx.snap @@ -424,7 +424,7 @@ exports[` spec renders the component, validation loading 1`]

- - Days + -

@@ -458,7 +458,7 @@ exports[` spec renders the component, validation loading 1`]

- - MB + -

@@ -492,7 +492,7 @@ exports[` spec renders the component, validation loading 1`]

- - Days + -

@@ -1518,7 +1518,7 @@ exports[`issue in detector validation issues in feature query 1`] = `

- - Days + -

@@ -1552,7 +1552,7 @@ exports[`issue in detector validation issues in feature query 1`] = `

- - MB + -

@@ -1586,7 +1586,7 @@ exports[`issue in detector validation issues in feature query 1`] = `

- - Days + -

diff --git a/public/redux/reducers/__tests__/anomalyResults.test.ts b/public/redux/reducers/__tests__/anomalyResults.test.ts index e668e2d1..ff6646df 100644 --- a/public/redux/reducers/__tests__/anomalyResults.test.ts +++ b/public/redux/reducers/__tests__/anomalyResults.test.ts @@ -18,7 +18,10 @@ import { mockedStore } from '../../utils/testUtils'; import reducer, { getDetectorResults, initialDetectorsState, + searchResults, } from '../anomalyResults'; +import { ALL_CUSTOM_AD_RESULT_INDICES } from '../../../pages/utils/constants' +import { getAnomalySummaryQuery } from '../../../pages/utils/anomalyResultUtils' jest.mock('../../../services', () => ({ ...jest.requireActual('../../../services'), @@ -78,7 +81,7 @@ describe('anomaly results reducer actions', () => { expect(httpMockedClient.get).toHaveBeenCalledWith( `..${ AD_NODE_API.DETECTOR - }/${tempDetectorId}/results/${false}/${resultIndex}/true`, + }/${tempDetectorId}/results/${false}/${resultIndex}*/true`, { query: queryParams } ); }); @@ -117,5 +120,161 @@ describe('anomaly results reducer actions', () => { ); } }); + test('result index pattern will not result in appended wildcard star', async () => { + const response = { + totalAnomalies: 1, + results: [{ anomalyGrade: 0, confidence: 1, starTime: 1, endTime: 2 }], + }; + httpMockedClient.get = jest + .fn() + .mockResolvedValue({ ok: true, response }); + const tempDetectorId = '123'; + let queryParams: DetectorResultsQueryParams = { + from: 0, + size: 20, + sortDirection: SORT_DIRECTION.ASC, + sortField: 'startTime', + }; + await store.dispatch( + getDetectorResults( + tempDetectorId, + '', + queryParams, + false, + ALL_CUSTOM_AD_RESULT_INDICES, + true + ) + ); + const actions = store.getActions(); + + expect(actions[0].type).toBe('ad/DETECTOR_RESULTS_REQUEST'); + expect(reducer(initialDetectorsState, actions[0])).toEqual({ + ...initialDetectorsState, + requesting: true, + }); + expect(actions[1].type).toBe('ad/DETECTOR_RESULTS_SUCCESS'); + expect(reducer(initialDetectorsState, actions[1])).toEqual({ + ...initialDetectorsState, + requesting: false, + total: response.totalAnomalies, + anomalies: response.results, + featureData: undefined, + }); + expect(httpMockedClient.get).toHaveBeenCalledWith( + `..${ + AD_NODE_API.DETECTOR + }/${tempDetectorId}/results/${false}/${ALL_CUSTOM_AD_RESULT_INDICES}/true`, + { query: queryParams } + ); + }); + }); + test('searchResults should append wildcard star at the end of custom result index', async () => { + const response = { + aggregations: { + top_entities: { + doc_count: 0, + top_entity_aggs: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [] + } + } + } + }; + + httpMockedClient.post = jest + .fn() + .mockResolvedValue({ ok: true, response }); + const tempDetectorId = '123'; + const resultIndex = 'opensearch-ad-plugin-result-test'; + const requestBody = getAnomalySummaryQuery(1717529636479, 1717529736479, tempDetectorId, undefined, false, undefined, undefined) + await store.dispatch( + searchResults( + requestBody, + resultIndex, + '', + true + ) + ); + const actions = store.getActions(); + + expect(actions[0].type).toBe('ad/SEARCH_ANOMALY_RESULTS_REQUEST'); + expect(reducer(initialDetectorsState, actions[0])).toEqual({ + ...initialDetectorsState, + requesting: true, + }); + expect(actions[1].type).toBe('ad/SEARCH_ANOMALY_RESULTS_SUCCESS'); + expect(reducer(initialDetectorsState, actions[1])).toEqual({ + ...initialDetectorsState, + requesting: false, + }); + expect(httpMockedClient.post).toHaveBeenCalledWith( + `..${ + AD_NODE_API.DETECTOR + }/results/_search/${resultIndex}*/true`, + { body: JSON.stringify(requestBody) } + ); + }); + test('searchResults should not append wildcard star at the end of custom result index', async () => { + const response = { + took: 1, + timed_out: false, + _shards: { + total: 2, + successful: 2, + skipped: 0, + failed: 0 + }, + hits: { + total: { + value: 0, + relation: "eq" + }, + max_score: null, + hits: [] + }, + aggregations: { + top_entities: { + doc_count: 0, + top_entity_aggs: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [] + } + } + } + }; + + httpMockedClient.post = jest + .fn() + .mockResolvedValue({ ok: true, response }); + const tempDetectorId = '123'; + const requestBody = getAnomalySummaryQuery(1717529636479, 1717529736479, tempDetectorId, undefined, false, undefined, undefined) + await store.dispatch( + searchResults( + requestBody, + ALL_CUSTOM_AD_RESULT_INDICES, + '', + true + ) + ); + const actions = store.getActions(); + + expect(actions[0].type).toBe('ad/SEARCH_ANOMALY_RESULTS_REQUEST'); + expect(reducer(initialDetectorsState, actions[0])).toEqual({ + ...initialDetectorsState, + requesting: true, + }); + expect(actions[1].type).toBe('ad/SEARCH_ANOMALY_RESULTS_SUCCESS'); + expect(reducer(initialDetectorsState, actions[1])).toEqual({ + ...initialDetectorsState, + requesting: false, + }); + expect(httpMockedClient.post).toHaveBeenCalledWith( + `..${ + AD_NODE_API.DETECTOR + }/results/_search/${ALL_CUSTOM_AD_RESULT_INDICES}/true`, + { body: JSON.stringify(requestBody) } + ); }); }); diff --git a/public/redux/reducers/__tests__/liveAnomalyResults.test.ts b/public/redux/reducers/__tests__/liveAnomalyResults.test.ts new file mode 100644 index 00000000..a6f359b7 --- /dev/null +++ b/public/redux/reducers/__tests__/liveAnomalyResults.test.ts @@ -0,0 +1,136 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +import { MockStore } from 'redux-mock-store'; +import { DetectorResultsQueryParams } from '../../../../server/models/types'; +import { SORT_DIRECTION } from '../../../../server/utils/constants'; +import httpMockedClient from '../../../../test/mocks/httpClientMock'; +import { AD_NODE_API } from '../../../../utils/constants'; +import { mockedStore } from '../../utils/testUtils'; +import { ALL_CUSTOM_AD_RESULT_INDICES } from '../../../pages/utils/constants'; +import reducer, { + getDetectorLiveResults, + initialDetectorLiveResults, +} from '../liveAnomalyResults'; + +jest.mock('../../../services', () => ({ + ...jest.requireActual('../../../services'), + + getDataSourceEnabled: () => ({ + enabled: false, + }), +})); + +describe('live anomaly results reducer actions', () => { + let store: MockStore; + beforeEach(() => { + store = mockedStore(); + }); + describe('getDetectorLiveResults', () => { + test('getDetectorLiveResults should append wildcard star at the end of custom result index', async () => { + const response = { + totalAnomalies: 1, + results: [{ anomalyGrade: 0, confidence: 1, starTime: 1, endTime: 2 }], + }; + + httpMockedClient.get = jest + .fn() + .mockResolvedValue({ ok: true, response }); + const tempDetectorId = '123'; + const resultIndex = 'opensearch-ad-plugin-result-test'; + let queryParams: DetectorResultsQueryParams = { + from: 0, + size: 20, + sortDirection: SORT_DIRECTION.ASC, + sortField: 'startTime', + }; + await store.dispatch( + getDetectorLiveResults( + tempDetectorId, + '', + queryParams, + false, + resultIndex, + true + ) + ); + const actions = store.getActions(); + + expect(actions[0].type).toBe('ad/DETECTOR_LIVE_RESULTS_REQUEST'); + expect(reducer(initialDetectorLiveResults, actions[0])).toEqual({ + ...initialDetectorLiveResults, + requesting: true, + }); + expect(actions[1].type).toBe('ad/DETECTOR_LIVE_RESULTS_SUCCESS'); + expect(reducer(initialDetectorLiveResults, actions[1])).toEqual({ + ...initialDetectorLiveResults, + requesting: false, + totalLiveAnomalies: response.totalAnomalies, + liveAnomalies: response.results, + errorMessage: '', + }); + expect(httpMockedClient.get).toHaveBeenCalledWith( + `..${ + AD_NODE_API.DETECTOR + }/${tempDetectorId}/results/${false}/${resultIndex}*/true`, + { query: queryParams } + ); + }); + test('getDetectorLiveResults should not append wildcard star at the end of custom result index', async () => { + const response = { + totalAnomalies: 1, + results: [{ anomalyGrade: 0, confidence: 1, starTime: 1, endTime: 2 }], + }; + + httpMockedClient.get = jest + .fn() + .mockResolvedValue({ ok: true, response }); + const tempDetectorId = '123'; + let queryParams: DetectorResultsQueryParams = { + from: 0, + size: 20, + sortDirection: SORT_DIRECTION.ASC, + sortField: 'startTime', + }; + await store.dispatch( + getDetectorLiveResults( + tempDetectorId, + '', + queryParams, + false, + ALL_CUSTOM_AD_RESULT_INDICES, + true + ) + ); + const actions = store.getActions(); + + expect(actions[0].type).toBe('ad/DETECTOR_LIVE_RESULTS_REQUEST'); + expect(reducer(initialDetectorLiveResults, actions[0])).toEqual({ + ...initialDetectorLiveResults, + requesting: true, + }); + expect(actions[1].type).toBe('ad/DETECTOR_LIVE_RESULTS_SUCCESS'); + expect(reducer(initialDetectorLiveResults, actions[1])).toEqual({ + ...initialDetectorLiveResults, + requesting: false, + totalLiveAnomalies: response.totalAnomalies, + liveAnomalies: response.results, + errorMessage: '', + }); + expect(httpMockedClient.get).toHaveBeenCalledWith( + `..${ + AD_NODE_API.DETECTOR + }/${tempDetectorId}/results/${false}/${ALL_CUSTOM_AD_RESULT_INDICES}/true`, + { query: queryParams } + ); + }); + }); +}); diff --git a/public/redux/reducers/anomalyResults.ts b/public/redux/reducers/anomalyResults.ts index bf643a3c..0643db64 100644 --- a/public/redux/reducers/anomalyResults.ts +++ b/public/redux/reducers/anomalyResults.ts @@ -103,6 +103,12 @@ export const getDetectorResults = ( let url = `..${AD_NODE_API.DETECTOR}/${id}/results/${isHistorical}`; if (resultIndex) { + // search for custom index pattern instead of specific index/alias + // as a custom index will be rolled over and we don't want to lose + // history + if (!resultIndex.endsWith('*')) { + resultIndex += '*'; + } url += `/${resultIndex}/${onlyQueryCustomResultIndex}`; } @@ -125,6 +131,12 @@ export const searchResults = ( let baseUrl = `..${AD_NODE_API.DETECTOR}/results/_search`; if (resultIndex) { + // search for custom index pattern instead of specific index/alias + // as a custom index will be rolled over and we don't want to lose + // history + if (!resultIndex.endsWith('*')) { + resultIndex += '*'; + } baseUrl += `/${resultIndex}/${onlyQueryCustomResultIndex}`; } diff --git a/public/redux/reducers/liveAnomalyResults.ts b/public/redux/reducers/liveAnomalyResults.ts index aa06efef..482fc286 100644 --- a/public/redux/reducers/liveAnomalyResults.ts +++ b/public/redux/reducers/liveAnomalyResults.ts @@ -66,6 +66,12 @@ export const getDetectorLiveResults = ( let url = `..${AD_NODE_API.DETECTOR}/${detectorId}/results/${isHistorical}`; if (resultIndex) { + // search for custom index pattern instead of specific index/alias + // as a custom index will be rolled over and we don't want to lose + // history + if (!resultIndex.endsWith('*')) { + resultIndex += '*'; + } url += `/${resultIndex}/${onlyQueryCustomResultIndex}`; }