diff --git a/.github/workflows/remote-integ-tests-workflow.yml b/.github/workflows/remote-integ-tests-workflow.yml index 63b87b46..7450f85f 100644 --- a/.github/workflows/remote-integ-tests-workflow.yml +++ b/.github/workflows/remote-integ-tests-workflow.yml @@ -71,36 +71,48 @@ jobs: node-version: ${{ steps.tool-versions.outputs.node_version }} registry-url: 'https://registry.npmjs.org' - - name: Setup Opensearch Dashboards + - name: Install correct yarn version for OpenSearch Dashboards + id: setup-yarn run: | npm uninstall -g yarn - echo "Installing yarn ${{ steps.tool-versions.outputs.yarn_version }}" - npm i -g yarn@${{ steps.tool-versions.outputs.yarn_version }} - yarn cache clean - yarn add sha.js - working-directory: OpenSearch-Dashboards - shell: bash + echo "Installing yarn ${{ steps.versions_step.outputs.yarn_version }}" + npm i -g yarn@${{ steps.versions_step.outputs.yarn_version }} - - name: Boodstrap Opensearch Dashboards + - name: Yarn Cache + uses: actions/cache@v4 + with: + path: | + OpenSearch-Dashboards/**/target + OpenSearch-Dashboards/**/node_modules + key: ${{ runner.OS }}-build-${{ hashFiles('OpenSearch-Dashboards/**/yarn.lock') }} + restore-keys: | + ${{ runner.OS }}-build- + + - name: Bootstrap OpenSearch Dashboards run: | + cd OpenSearch-Dashboards yarn osd bootstrap --single-version=loose - working-directory: OpenSearch-Dashboards/plugins/anomaly-detection-dashboards-plugin + + - name: Compile OpenSearch Dashboards + run: | + cd OpenSearch-Dashboards + node scripts/build_opensearch_dashboards_platform_plugins --no-examples --workers=10 --verbose - name: Run Opensearch Dashboards with AD Installed run: | nohup yarn start --no-base-path --no-watch --server.host="0.0.0.0" | tee dashboard.log & working-directory: OpenSearch-Dashboards - - name : Check If OpenSearch Dashboards Is Ready + - name: Check If OpenSearch Dashboards Is Ready if: ${{ runner.os == 'Linux' }} run: | - if timeout 600 grep -q "bundles compiled successfully after" <(tail -n0 -f dashboard.log); then - echo "OpenSearch Dashboards compiled successfully." + cd ./OpenSearch-Dashboards + if timeout 60 grep -q "http server running" <(tail -n +1 -f dashboard.log); then + echo "OpenSearch Dashboards started successfully." else - echo "Timeout for 600 seconds reached. OpenSearch Dashboards did not finish compiling." + echo "Timeout of 60 seconds reached. OpenSearch Dashboards did not start successfully." exit 1 - fi - working-directory: OpenSearch-Dashboards + fi& - name: Show OpenSearch Dashboards Logs if: always() diff --git a/public/pages/ConfigureModel/containers/ConfigureModel.tsx b/public/pages/ConfigureModel/containers/ConfigureModel.tsx index c7867869..1c0c0cd3 100644 --- a/public/pages/ConfigureModel/containers/ConfigureModel.tsx +++ b/public/pages/ConfigureModel/containers/ConfigureModel.tsx @@ -456,6 +456,8 @@ export function ConfigureModel(props: ConfigureModelProps) { categoryFields={formikProps.values.categoryField} errors={formikProps.errors} setFieldTouched={formikProps.setFieldTouched} + imputationOption={formikProps.values.imputationOption} + suppressionRules={formikProps.values.suppressionRules} /> ) : null} diff --git a/public/pages/ConfigureModel/containers/SampleAnomalies.tsx b/public/pages/ConfigureModel/containers/SampleAnomalies.tsx index 9954573e..5a06dbdc 100644 --- a/public/pages/ConfigureModel/containers/SampleAnomalies.tsx +++ b/public/pages/ConfigureModel/containers/SampleAnomalies.tsx @@ -44,7 +44,7 @@ import { } from '../../utils/anomalyResultUtils'; import { focusOnFirstWrongFeature } from '../utils/helpers'; import { prepareDetector } from '../utils/helpers'; -import { FeaturesFormikValues } from '../models/interfaces'; +import { FeaturesFormikValues, ImputationFormikValues, RuleFormikValues} from '../models/interfaces'; import { BASE_DOCS_LINK } from '../../../utils/constants'; import { prettifyErrorMessage } from '../../../../server/utils/helpers'; import { CoreStart } from '../../../../../../src/core/public'; @@ -59,6 +59,8 @@ interface SampleAnomaliesProps { categoryFields: string[]; errors: any; setFieldTouched: any; + imputationOption?: ImputationFormikValues; + suppressionRules?: RuleFormikValues[]; } export function SampleAnomalies(props: SampleAnomaliesProps) { @@ -183,7 +185,9 @@ export function SampleAnomalies(props: SampleAnomaliesProps) { props.shingleSize, props.categoryFields, newDetector, - true + true, + props.imputationOption, + props.suppressionRules, ); setPreviewDone(false); setZoomRange({ ...dateRange }); diff --git a/public/pages/ConfigureModel/containers/__tests__/SampleAnomalies.test.tsx b/public/pages/ConfigureModel/containers/__tests__/SampleAnomalies.test.tsx new file mode 100644 index 00000000..39d91482 --- /dev/null +++ b/public/pages/ConfigureModel/containers/__tests__/SampleAnomalies.test.tsx @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: Apache-2.0 + +import React from 'react'; +import { Provider } from 'react-redux'; +import { + HashRouter as Router, + RouteComponentProps, + Route, + Switch, +} from 'react-router-dom'; +import { render, fireEvent, waitFor } from '@testing-library/react'; +import { SampleAnomalies } from '../SampleAnomalies'; +import configureStore from '../../../../redux/configureStore'; +import { httpClientMock, coreServicesMock } from '../../../../../test/mocks'; +import { mockedStore } from '../../../../redux/utils/testUtils'; +import { CoreServicesContext } from '../../../../components/CoreServices/CoreServices'; +import { prepareDetector, focusOnFirstWrongFeature } from '../../utils/helpers'; +import { createMemoryHistory } from 'history'; +import { + FeaturesFormikValues, + ImputationFormikValues, + RuleFormikValues, +} from '../../../../pages/ConfigureModel/models/interfaces'; +import { getRandomDetector } from '../../../../redux/reducers/__tests__/utils'; +import { FEATURE_TYPE } from '../../../../models/interfaces'; + +// Mock the helper functions +jest.mock('../../utils/helpers', () => ({ + ...jest.requireActual('../../utils/helpers'), + prepareDetector: jest.fn(() => ({ + // mock a detector object with at least an id, preventing the undefined error when detector.id is accessed in getSampleAdResult. + id: 'test-detector-id', + name: 'test-detector', + description: 'test-detector-description', + })), + focusOnFirstWrongFeature: jest.fn(() => false), +})); + +// Mock the Redux actions if necessary +jest.mock('../../../../redux/reducers/previewAnomalies', () => ({ + previewDetector: jest.fn(() => async () => Promise.resolve()), +})); + +describe(' spec', () => { + beforeEach(() => { + jest.clearAllMocks(); + console.error = jest.fn(); + console.warn = jest.fn(); + }); + + test('calls prepareDetector with imputationOption and suppressionRules when previewDetector button is clicked', async () => { + // Set up the mock props + const mockDetector = getRandomDetector(); + + const mockFeatureList: FeaturesFormikValues[] = [ + { + featureId: 'feature1', + featureName: 'Feature 1', + featureEnabled: true, + aggregationBy: 'sum', + aggregationOf: [{ label: 'bytes' }], + featureType: FEATURE_TYPE.SIMPLE, + aggregationQuery: '', + }, + ]; + + const mockImputationOption: ImputationFormikValues = { + imputationMethod: 'zero', + }; + + const mockSuppressionRules: RuleFormikValues[] = [ + { + featureName: '', + absoluteThreshold: undefined, + relativeThreshold: undefined, + aboveBelow: 'above', + }, + ]; + + const props = { + detector: mockDetector, + featureList: mockFeatureList, + shingleSize: 8, + categoryFields: ['category1'], + errors: {}, + setFieldTouched: jest.fn(), + imputationOption: mockImputationOption, + suppressionRules: mockSuppressionRules, + }; + + // Create a mock history and store + const history = createMemoryHistory(); + const initialState = { + anomalies: { + anomaliesResult: { + anomalies: [], + }, + }, + }; + const store = mockedStore(); + + const { getByText } = render( + + + + + + + + ); + + // Simulate clicking the previewDetector button + fireEvent.click(getByText('Preview anomalies')); + + // Wait for async actions to complete + await waitFor(() => { + expect(prepareDetector).toHaveBeenCalledWith( + props.featureList, + props.shingleSize, + props.categoryFields, + expect.anything(), // newDetector (could be the same as props.detector or modified) + true, + props.imputationOption, + props.suppressionRules + ); + }); + }); +}); diff --git a/public/pages/ConfigureModel/utils/__tests__/helpers.test.tsx b/public/pages/ConfigureModel/utils/__tests__/helpers.test.tsx index 0f8798c0..c78e4332 100644 --- a/public/pages/ConfigureModel/utils/__tests__/helpers.test.tsx +++ b/public/pages/ConfigureModel/utils/__tests__/helpers.test.tsx @@ -30,12 +30,12 @@ describe('featuresToFormik', () => { aggregationQuery: '', }; const randomPositiveInt = Math.ceil(Math.random() * 100); - const apiRequest = prepareDetector( - [newFeature], - randomPositiveInt, - randomDetector, - false - ); + // const apiRequest = prepareDetector( + // [newFeature], + // randomPositiveInt, + // randomDetector, + // false + // ); // expect(apiRequest.featureAttributes).toEqual([ // { // featureId: newFeature.featureId, diff --git a/public/pages/ConfigureModel/utils/helpers.ts b/public/pages/ConfigureModel/utils/helpers.ts index c88c135e..7df261a1 100644 --- a/public/pages/ConfigureModel/utils/helpers.ts +++ b/public/pages/ConfigureModel/utils/helpers.ts @@ -340,7 +340,9 @@ export function prepareDetector( shingleSizeValue: number, categoryFields: string[], ad: Detector, - forPreview: boolean = false + forPreview: boolean = false, + imputationOption?: ImputationFormikValues, + suppressionRules?: RuleFormikValues[] ): Detector { const detector = cloneDeep(ad); const featureAttributes = formikToFeatures(featureValues, forPreview); @@ -354,6 +356,8 @@ export function prepareDetector( ...detector.uiMetadata, features: { ...featuresToUIMetadata(featureValues) }, }, + imputationOption: formikToImputationOption(imputationOption), + rules: formikToRules(suppressionRules), }; }