diff --git a/package.json b/package.json
index da506ddc..4bb29f82 100644
--- a/package.json
+++ b/package.json
@@ -27,12 +27,15 @@
},
"devDependencies": {
"@testing-library/user-event": "^13.1.9",
+ "@types/enzyme-adapter-react-16": "^1.0.6",
+ "@types/react-test-renderer": "^16.9.1",
"cypress": "^5.0.0",
"eslint": "^6.8.0",
"husky": "^4.2.5",
"jest-raw-loader": "^1.0.1",
"lint-staged": "^10.2.0",
"mutationobserver-shim": "^0.3.3",
+ "jest-dom": "^4.0.0",
"ts-jest": "^29.1.0"
},
"resolutions": {
diff --git a/public/ace-themes/sql_console.js b/public/ace-themes/sql_console.js
index c841db28..b0d4898c 100644
--- a/public/ace-themes/sql_console.js
+++ b/public/ace-themes/sql_console.js
@@ -3,10 +3,13 @@
* SPDX-License-Identifier: Apache-2.0
*/
-
import * as ace from 'brace';
-ace.define('ace/theme/sql_console', ['require', 'exports', 'module', 'ace/lib/dom'], function (acequire, exports, module) {
+ace.define('ace/theme/sql_console', ['require', 'exports', 'module', 'ace/lib/dom'], function (
+ acequire,
+ exports,
+ module
+) {
exports.isDark = false;
exports.cssClass = 'ace-sql-console';
exports.cssText = require('../index.scss');
diff --git a/public/components/acceleration/create/__tests__/__snapshots__/caution_banner_callout.test.tsx.snap b/public/components/acceleration/create/__tests__/__snapshots__/caution_banner_callout.test.tsx.snap
new file mode 100644
index 00000000..891e2965
--- /dev/null
+++ b/public/components/acceleration/create/__tests__/__snapshots__/caution_banner_callout.test.tsx.snap
@@ -0,0 +1,3 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Acceleration callout renders acceleration flyout callout 1`] = `ReactWrapper {}`;
diff --git a/public/components/acceleration/create/__tests__/__snapshots__/create_acceleration.test.tsx.snap b/public/components/acceleration/create/__tests__/__snapshots__/create_acceleration.test.tsx.snap
new file mode 100644
index 00000000..32dc90f7
--- /dev/null
+++ b/public/components/acceleration/create/__tests__/__snapshots__/create_acceleration.test.tsx.snap
@@ -0,0 +1,3 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Create acceleration flyout components renders acceleration flyout component with default options 1`] = `ReactWrapper {}`;
diff --git a/public/components/acceleration/create/__tests__/__snapshots__/create_acceleration_header.test.tsx.snap b/public/components/acceleration/create/__tests__/__snapshots__/create_acceleration_header.test.tsx.snap
new file mode 100644
index 00000000..4b3b9f03
--- /dev/null
+++ b/public/components/acceleration/create/__tests__/__snapshots__/create_acceleration_header.test.tsx.snap
@@ -0,0 +1,3 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Acceleration header renders acceleration flyout header 1`] = `ReactWrapper {}`;
diff --git a/public/components/acceleration/create/__tests__/caution_banner_callout.test.tsx b/public/components/acceleration/create/__tests__/caution_banner_callout.test.tsx
new file mode 100644
index 00000000..f3888a2b
--- /dev/null
+++ b/public/components/acceleration/create/__tests__/caution_banner_callout.test.tsx
@@ -0,0 +1,22 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { waitFor } from '@testing-library/dom';
+import { configure, mount } from 'enzyme';
+import Adapter from 'enzyme-adapter-react-16';
+import React from 'react';
+import { CautionBannerCallout } from '../caution_banner_callout';
+
+describe('Acceleration callout', () => {
+ configure({ adapter: new Adapter() });
+
+ it('renders acceleration flyout callout', async () => {
+ const wrapper = mount(() as React.ReactElement);
+ wrapper.update();
+ await waitFor(() => {
+ expect(wrapper).toMatchSnapshot();
+ });
+ });
+});
diff --git a/public/components/acceleration/create/__tests__/create_acceleration.test.tsx b/public/components/acceleration/create/__tests__/create_acceleration.test.tsx
new file mode 100644
index 00000000..1261e060
--- /dev/null
+++ b/public/components/acceleration/create/__tests__/create_acceleration.test.tsx
@@ -0,0 +1,33 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { waitFor } from '@testing-library/dom';
+import { configure, mount } from 'enzyme';
+import Adapter from 'enzyme-adapter-react-16';
+import React from 'react';
+import { CreateAcceleration } from '../create_acceleration';
+
+describe('Create acceleration flyout components', () => {
+ configure({ adapter: new Adapter() });
+
+ it('renders acceleration flyout component with default options', async () => {
+ const dataSource = '';
+ const resetFlyout = jest.fn();
+ const updateQueries = jest.fn();
+ const wrapper = mount(
+ (
+
+ ) as React.ReactElement
+ );
+ wrapper.update();
+ await waitFor(() => {
+ expect(wrapper).toMatchSnapshot();
+ });
+ });
+});
diff --git a/public/components/acceleration/create/__tests__/create_acceleration_header.test.tsx b/public/components/acceleration/create/__tests__/create_acceleration_header.test.tsx
new file mode 100644
index 00000000..fe33575c
--- /dev/null
+++ b/public/components/acceleration/create/__tests__/create_acceleration_header.test.tsx
@@ -0,0 +1,22 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { waitFor } from '@testing-library/dom';
+import { configure, mount } from 'enzyme';
+import Adapter from 'enzyme-adapter-react-16';
+import React from 'react';
+import { CreateAccelerationHeader } from '../create_acceleration_header';
+
+describe('Acceleration header', () => {
+ configure({ adapter: new Adapter() });
+
+ it('renders acceleration flyout header', async () => {
+ const wrapper = mount(() as React.ReactElement);
+ wrapper.update();
+ await waitFor(() => {
+ expect(wrapper).toMatchSnapshot();
+ });
+ });
+});
diff --git a/public/components/acceleration/create/__tests__/utils.test.tsx b/public/components/acceleration/create/__tests__/utils.test.tsx
new file mode 100644
index 00000000..060d6385
--- /dev/null
+++ b/public/components/acceleration/create/__tests__/utils.test.tsx
@@ -0,0 +1,262 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {
+ ACCELERATION_INDEX_NAME_REGEX,
+ ACCELERATION_S3_URL_REGEX,
+} from '../../../../../common/constants';
+import {
+ coveringIndexDataMock,
+ materializedViewEmptyDataMock,
+ materializedViewEmptyTumbleDataMock,
+ materializedViewStaleDataMock,
+ materializedViewValidDataMock,
+ skippingIndexDataMock,
+} from '../../../../../test/mocks/accelerationMock';
+import {
+ pluralizeTime,
+ validateCheckpointLocation,
+ validateCoveringIndexData,
+ validateDataSource,
+ validateDataTable,
+ validateDatabase,
+ validateIndexName,
+ validateMaterializedViewData,
+ validatePrimaryShardCount,
+ validateRefreshInterval,
+ validateReplicaCount,
+ validateSkippingIndexData,
+} from '../utils';
+
+describe('pluralizeTime', () => {
+ it('should return "s" for a time window greater than 1', () => {
+ expect(pluralizeTime(2)).toBe('s');
+ expect(pluralizeTime(10)).toBe('s');
+ expect(pluralizeTime(100)).toBe('s');
+ });
+
+ it('should return an empty string for a time window of 1/0', () => {
+ expect(pluralizeTime(1)).toBe('');
+ expect(pluralizeTime(0)).toBe(''); // form throws validation error, doesn't allow user to proceed
+ });
+});
+
+describe('validateDataSource', () => {
+ it('should return an array with an error message when the dataSource is empty', () => {
+ expect(validateDataSource('')).toEqual(['Select a valid data source']);
+ expect(validateDataSource(' ')).toEqual(['Select a valid data source']);
+ });
+
+ it('should return an empty array when the dataSource is not empty', () => {
+ expect(validateDataSource('Some_valid_data_source')).toEqual([]);
+ expect(validateDataSource(' Some_valid_data_source ')).toEqual([]);
+ });
+});
+
+describe('validateDatabase', () => {
+ it('should return an array with an error message when the database is empty', () => {
+ expect(validateDatabase('')).toEqual(['Select a valid database']);
+ expect(validateDatabase(' ')).toEqual(['Select a valid database']);
+ });
+
+ it('should return an empty array when the database is not empty', () => {
+ expect(validateDatabase('Some_valid_database')).toEqual([]);
+ expect(validateDatabase(' Some_valid_database ')).toEqual([]);
+ });
+});
+
+describe('validateDataTable', () => {
+ it('should return an array with an error message when the dataTable is empty', () => {
+ expect(validateDataTable('')).toEqual(['Select a valid table']);
+ expect(validateDataTable(' ')).toEqual(['Select a valid table']);
+ });
+
+ it('should return an empty array when the dataTable is not empty', () => {
+ expect(validateDataTable('Some_valid_table')).toEqual([]);
+ expect(validateDataTable(' Some_valid_table ')).toEqual([]);
+ });
+});
+
+describe('validatePrimaryShardCount', () => {
+ it('should return an array with an error message when primaryShardCount is less than 1', () => {
+ expect(validatePrimaryShardCount(0)).toEqual(['Primary shards count should be greater than 0']);
+ expect(validatePrimaryShardCount(-1)).toEqual([
+ 'Primary shards count should be greater than 0',
+ ]); // form throws validation error, doesn't allow user to proceed
+ });
+
+ it('should return an empty array when primaryShardCount is greater than or equal to 1', () => {
+ expect(validatePrimaryShardCount(1)).toEqual([]);
+ expect(validatePrimaryShardCount(5)).toEqual([]);
+ expect(validatePrimaryShardCount(100)).toEqual([]);
+ });
+});
+
+describe('validateReplicaCount', () => {
+ it('should return an array with an error message when replicaCount is less than 1', () => {
+ expect(validateReplicaCount(0)).toEqual(['Replica count should be greater than 0']);
+ expect(validateReplicaCount(-1)).toEqual(['Replica count should be greater than 0']); // form throws validation error, doesn't allow user to proceed
+ });
+
+ it('should return an empty array when replicaCount is greater than or equal to 1', () => {
+ expect(validateReplicaCount(1)).toEqual([]);
+ expect(validateReplicaCount(5)).toEqual([]);
+ expect(validateReplicaCount(100)).toEqual([]);
+ });
+});
+
+describe('validateRefreshInterval', () => {
+ it('should return an array with an error message when refreshType is "interval" and refreshWindow is less than 1', () => {
+ expect(validateRefreshInterval('interval', 0)).toEqual([
+ 'refresh window should be greater than 0',
+ ]);
+ expect(validateRefreshInterval('interval', -1)).toEqual([
+ 'refresh window should be greater than 0',
+ ]);
+ expect(validateRefreshInterval('interval', -10)).toEqual([
+ 'refresh window should be greater than 0',
+ ]);
+ });
+
+ it('should return an empty array when refreshType is not "interval" or when refreshWindow is greater than or equal to 1', () => {
+ expect(validateRefreshInterval('auto', 0)).toEqual([]);
+ expect(validateRefreshInterval('auto', 1)).toEqual([]);
+ expect(validateRefreshInterval('interval', 1)).toEqual([]);
+ expect(validateRefreshInterval('auto', 5)).toEqual([]);
+ });
+});
+
+describe('validateIndexName', () => {
+ it('should return an array with an error message when the index name is invalid', () => {
+ expect(validateIndexName('_invalid')).toEqual(['Enter a valid index name']);
+ expect(validateIndexName('-invalid')).toEqual(['Enter a valid index name']);
+ expect(validateIndexName('InVal1d')).toEqual(['Enter a valid index name']);
+ expect(validateIndexName('invalid_with spaces')).toEqual(['Enter a valid index name']);
+ });
+
+ it('should return an empty array when the index name is valid', () => {
+ expect(validateIndexName('valid')).toEqual([]);
+ expect(validateIndexName('valid_name')).toEqual([]);
+ expect(validateIndexName('another-valid-name')).toEqual([]);
+ });
+
+ it('should use the ACCELERATION_INDEX_NAME_REGEX pattern to validate the index name', () => {
+ expect(ACCELERATION_INDEX_NAME_REGEX.test('valid_name')).toBe(true);
+ expect(ACCELERATION_INDEX_NAME_REGEX.test('invalid name')).toBe(false);
+ expect(ACCELERATION_INDEX_NAME_REGEX.test('-invalid')).toBe(false);
+ expect(ACCELERATION_INDEX_NAME_REGEX.test('_invalid')).toBe(false);
+ expect(ACCELERATION_INDEX_NAME_REGEX.test('invalid.')).toBe(false);
+ expect(ACCELERATION_INDEX_NAME_REGEX.test('invalid<')).toBe(false);
+ expect(ACCELERATION_INDEX_NAME_REGEX.test('invalid*')).toBe(false);
+ });
+});
+
+describe('validateCheckpointLocation', () => {
+ it('should return an array with an error message when creating a materialized view without a checkpoint location', () => {
+ const materializedError = validateCheckpointLocation('materialized', undefined);
+ expect(materializedError).toEqual([
+ 'Checkpoint location is mandatory for materialized view creation',
+ ]);
+ });
+
+ it('should return an array with an error message when creating a materialized view without a checkpoint location', () => {
+ const materializedError = validateCheckpointLocation('materialized', '');
+ expect(materializedError).toEqual([
+ 'Checkpoint location is mandatory for materialized view creation',
+ ]);
+ });
+
+ it('should return an array with an error message when the checkpoint location is not a valid S3 URL', () => {
+ const invalidCheckpoint = validateCheckpointLocation('skipping', 'not_a_valid_s3_url');
+ expect(invalidCheckpoint).toEqual(['Enter a valid checkpoint location']);
+ });
+
+ it('should return an empty array when the checkpoint location is a valid S3 URL', () => {
+ const validCheckpoint = validateCheckpointLocation(
+ 'covering',
+ 's3://valid-s3-bucket/path/to/checkpoint'
+ );
+ expect(validCheckpoint).toEqual([]);
+ });
+
+ it('should return an empty array when the checkpoint location is a valid S3A URL', () => {
+ const validCheckpoint = validateCheckpointLocation(
+ 'skipping',
+ 's3a://valid-s3-bucket/path/to/checkpoint'
+ );
+ expect(validCheckpoint).toEqual([]);
+ });
+
+ it('should return an empty array when creating a materialized view with a valid checkpoint location', () => {
+ const validMaterializedCheckpoint = validateCheckpointLocation(
+ 'materialized',
+ 's3://valid-s3-bucket/path/to/checkpoint'
+ );
+ expect(validMaterializedCheckpoint).toEqual([]);
+ });
+
+ it('should use the ACCELERATION_S3_URL_REGEX pattern to validate the checkpoint location', () => {
+ expect(ACCELERATION_S3_URL_REGEX.test('s3://valid-s3-bucket/path/to/checkpoint')).toBe(true);
+ expect(ACCELERATION_S3_URL_REGEX.test('s3a://valid-s3-bucket/path/to/checkpoint')).toBe(true);
+ expect(ACCELERATION_S3_URL_REGEX.test('https://amazon.com')).toBe(false);
+ expect(ACCELERATION_S3_URL_REGEX.test('http://www.amazon.com')).toBe(false);
+ });
+});
+
+describe('validateSkippingIndexData', () => {
+ it('should return an array with an error message when accelerationIndexType is "skipping" and no skipping index data is provided', () => {
+ const error = validateSkippingIndexData('skipping', []);
+ expect(error).toEqual(['Add fields to the skipping index definition']);
+ });
+
+ it('should return an empty array when accelerationIndexType is not "skipping"', () => {
+ const noError = validateSkippingIndexData('covering', []);
+ expect(noError).toEqual([]);
+ });
+
+ it('should return an empty array when accelerationIndexType is "skipping" and skipping index data is provided', () => {
+ const noError = validateSkippingIndexData('skipping', skippingIndexDataMock);
+ expect(noError).toEqual([]);
+ });
+});
+
+describe('validateCoveringIndexData', () => {
+ it('should return an array with an error message when accelerationIndexType is "covering" and no covering index data is provided', () => {
+ const error = validateCoveringIndexData('covering', []);
+ expect(error).toEqual(['Add fields to covering index definition']);
+ });
+
+ it('should return an empty array when accelerationIndexType is not "covering"', () => {
+ const noError = validateCoveringIndexData('skipping', []);
+ expect(noError).toEqual([]);
+ });
+
+ it('should return an empty array when accelerationIndexType is "covering" and covering index data is provided', () => {
+ const noError = validateCoveringIndexData('covering', coveringIndexDataMock);
+ expect(noError).toEqual([]);
+ });
+});
+
+describe('validateMaterializedViewData', () => {
+ it('should return an array with an error message when accelerationIndexType is "materialized" and no materialized view data is provided', () => {
+ const error = validateMaterializedViewData('materialized', materializedViewEmptyDataMock);
+ expect(error).toEqual(['Add columns to materialized view definition']);
+ });
+
+ it('should return an array with an error message when accelerationIndexType is "materialized" and groupByTumbleValue is incomplete', () => {
+ const error = validateMaterializedViewData('materialized', materializedViewEmptyTumbleDataMock);
+ expect(error).toEqual(['Add a time field to tumble function in materialized view definition']);
+ });
+
+ it('should return an empty array when accelerationIndexType is not "materialized"', () => {
+ const noError = validateMaterializedViewData('covering', materializedViewStaleDataMock);
+ expect(noError).toEqual([]);
+ });
+
+ it('should return an empty array when accelerationIndexType is "materialized" and materialized view data is complete', () => {
+ const noError = validateMaterializedViewData('materialized', materializedViewValidDataMock);
+ expect(noError).toEqual([]);
+ });
+});
diff --git a/public/components/acceleration/create/create_acceleration.tsx b/public/components/acceleration/create/create_acceleration.tsx
index f667661c..8dae138b 100644
--- a/public/components/acceleration/create/create_acceleration.tsx
+++ b/public/components/acceleration/create/create_acceleration.tsx
@@ -103,7 +103,7 @@ export const CreateAcceleration = ({
0)
+ if (materializedViewQueryData.groupByTumbleValue.tumbleWindow < 1)
return ['Add a valid time window to tumble function in materialized view definition'];
return [];
};
diff --git a/public/components/acceleration/visual_editors/covering_index/covering_index_builder.tsx b/public/components/acceleration/visual_editors/covering_index/covering_index_builder.tsx
index 0335f498..5968ca01 100644
--- a/public/components/acceleration/visual_editors/covering_index/covering_index_builder.tsx
+++ b/public/components/acceleration/visual_editors/covering_index/covering_index_builder.tsx
@@ -17,6 +17,7 @@ import {
import React, { useState } from 'react';
import { ACCELERATION_ADD_FIELDS_TEXT } from '../../../../../common/constants';
import { CreateAccelerationForm } from '../../../../../common/types';
+import { hasError } from '../../create/utils';
interface CoveringIndexBuilderProps {
accelerationFormData: CreateAccelerationForm;
@@ -74,7 +75,10 @@ export const CoveringIndexBuilder = ({
value={columnsValue}
isActive={isPopOverOpen}
onClick={() => setIsPopOverOpen(true)}
- isInvalid={columnsValue === ACCELERATION_ADD_FIELDS_TEXT}
+ isInvalid={
+ hasError(accelerationFormData.formErrors, 'coveringIndexError') &&
+ columnsValue === ACCELERATION_ADD_FIELDS_TEXT
+ }
/>
}
isOpen={isPopOverOpen}
diff --git a/public/components/acceleration/visual_editors/materialized_view/add_column_popover.tsx b/public/components/acceleration/visual_editors/materialized_view/add_column_popover.tsx
index 2eb28d1d..df6b276f 100644
--- a/public/components/acceleration/visual_editors/materialized_view/add_column_popover.tsx
+++ b/public/components/acceleration/visual_editors/materialized_view/add_column_popover.tsx
@@ -18,6 +18,7 @@ import {
htmlIdGenerator,
} from '@elastic/eui';
import { EuiComboBoxOptionOption } from '@opensearch-project/oui';
+import producer from 'immer';
import React, { ChangeEvent, useEffect, useState } from 'react';
import { ACCELERATION_AGGREGRATION_FUNCTIONS } from '../../../../../common/constants';
import {
@@ -25,6 +26,7 @@ import {
CreateAccelerationForm,
MaterializedViewColumn,
} from '../../../../../common/types';
+import { validateMaterializedViewData } from '../../create/utils';
interface AddColumnPopOverProps {
isColumnPopOverOpen: boolean;
@@ -32,6 +34,7 @@ interface AddColumnPopOverProps {
columnExpressionValues: MaterializedViewColumn[];
setColumnExpressionValues: React.Dispatch>;
accelerationFormData: CreateAccelerationForm;
+ setAccelerationFormData: React.Dispatch>;
}
export const AddColumnPopOver = ({
@@ -40,6 +43,7 @@ export const AddColumnPopOver = ({
columnExpressionValues,
setColumnExpressionValues,
accelerationFormData,
+ setAccelerationFormData,
}: AddColumnPopOverProps) => {
const [selectedFunction, setSelectedFunction] = useState([
ACCELERATION_AGGREGRATION_FUNCTIONS[0],
@@ -64,6 +68,31 @@ export const AddColumnPopOver = ({
setSeletedAlias(e.target.value);
};
+ const onAddExpression = () => {
+ const newColumnExpresionValue = [
+ ...columnExpressionValues,
+ {
+ id: htmlIdGenerator()(),
+ functionName: selectedFunction[0].label as AggregationFunctionType,
+ functionParam: selectedField[0].label,
+ fieldAlias: selectedAlias,
+ },
+ ];
+
+ setAccelerationFormData(
+ producer((accData) => {
+ accData.materializedViewQueryData.columnsValues = newColumnExpresionValue;
+ accData.formErrors.materializedViewError = validateMaterializedViewData(
+ accData.accelerationIndexType,
+ accData.materializedViewQueryData
+ );
+ })
+ );
+
+ setColumnExpressionValues(newColumnExpresionValue);
+ setIsColumnPopOverOpen(false);
+ };
+
useEffect(() => {
resetSelectedField();
}, []);
@@ -97,6 +126,7 @@ export const AddColumnPopOver = ({
options={ACCELERATION_AGGREGRATION_FUNCTIONS}
selectedOptions={selectedFunction}
onChange={setSelectedFunction}
+ isClearable={false}
/>
@@ -110,6 +140,7 @@ export const AddColumnPopOver = ({
]}
selectedOptions={selectedField}
onChange={setSelectedField}
+ isClearable={false}
/>
@@ -120,22 +151,7 @@ export const AddColumnPopOver = ({
>
- {
- setColumnExpressionValues([
- ...columnExpressionValues,
- {
- id: htmlIdGenerator()(),
- functionName: selectedFunction[0].label as AggregationFunctionType,
- functionParam: selectedField[0].label,
- fieldAlias: selectedAlias,
- },
- ]);
- setIsColumnPopOverOpen(false);
- }}
- >
+
Add
diff --git a/public/components/acceleration/visual_editors/materialized_view/column_expression.tsx b/public/components/acceleration/visual_editors/materialized_view/column_expression.tsx
index 0da3d65b..5f9a0909 100644
--- a/public/components/acceleration/visual_editors/materialized_view/column_expression.tsx
+++ b/public/components/acceleration/visual_editors/materialized_view/column_expression.tsx
@@ -13,6 +13,7 @@ import {
EuiFormRow,
EuiPopover,
} from '@elastic/eui';
+import producer from 'immer';
import _ from 'lodash';
import React, { useState } from 'react';
import { ACCELERATION_AGGREGRATION_FUNCTIONS } from '../../../../../common/constants';
@@ -21,6 +22,7 @@ import {
CreateAccelerationForm,
MaterializedViewColumn,
} from '../../../../../common/types';
+import { validateMaterializedViewData } from '../../create/utils';
interface ColumnExpressionProps {
index: number;
@@ -28,6 +30,7 @@ interface ColumnExpressionProps {
columnExpressionValues: MaterializedViewColumn[];
setColumnExpressionValues: React.Dispatch>;
accelerationFormData: CreateAccelerationForm;
+ setAccelerationFormData: React.Dispatch>;
}
export const ColumnExpression = ({
@@ -36,6 +39,7 @@ export const ColumnExpression = ({
columnExpressionValues,
setColumnExpressionValues,
accelerationFormData,
+ setAccelerationFormData,
}: ColumnExpressionProps) => {
const [isFunctionPopOverOpen, setIsFunctionPopOverOpen] = useState(false);
const [isAliasPopOverOpen, setIsAliasPopOverOpen] = useState(false);
@@ -46,6 +50,22 @@ export const ColumnExpression = ({
setColumnExpressionValues(updatedArray);
};
+ const onDeleteColumnExpression = () => {
+ const newColumnExpresionValue = [
+ ..._.filter(columnExpressionValues, (o) => o.id !== currentColumnExpressionValue.id),
+ ];
+ setAccelerationFormData(
+ producer((accData) => {
+ accData.materializedViewQueryData.columnsValues = newColumnExpresionValue;
+ accData.formErrors.materializedViewError = validateMaterializedViewData(
+ accData.accelerationIndexType,
+ accData.materializedViewQueryData
+ );
+ })
+ );
+ setColumnExpressionValues(newColumnExpresionValue);
+ };
+
return (
@@ -89,6 +109,7 @@ export const ColumnExpression = ({
index
)
}
+ isClearable={false}
/>
@@ -116,6 +137,7 @@ export const ColumnExpression = ({
index
)
}
+ isClearable={false}
/>
@@ -162,14 +184,7 @@ export const ColumnExpression = ({
{
- setColumnExpressionValues([
- ..._.filter(
- columnExpressionValues,
- (o) => o.id !== currentColumnExpressionValue.id
- ),
- ]);
- }}
+ onClick={onDeleteColumnExpression}
iconType="trash"
aria-label="delete-column-expression"
/>
diff --git a/public/components/acceleration/visual_editors/materialized_view/group_by_tumble_expression.tsx b/public/components/acceleration/visual_editors/materialized_view/group_by_tumble_expression.tsx
index dc853414..e1e25339 100644
--- a/public/components/acceleration/visual_editors/materialized_view/group_by_tumble_expression.tsx
+++ b/public/components/acceleration/visual_editors/materialized_view/group_by_tumble_expression.tsx
@@ -14,10 +14,11 @@ import {
EuiPopover,
EuiSelect,
} from '@elastic/eui';
-import React, { useEffect, useState } from 'react';
+import producer from 'immer';
+import React, { useState } from 'react';
import { ACCELERATION_TIME_INTERVAL } from '../../../../../common/constants';
import { CreateAccelerationForm, GroupByTumbleType } from '../../../../../common/types';
-import { pluralizeTime } from '../../create/utils';
+import { hasError, pluralizeTime, validateMaterializedViewData } from '../../create/utils';
interface GroupByTumbleExpressionProps {
accelerationFormData: CreateAccelerationForm;
@@ -35,29 +36,36 @@ export const GroupByTumbleExpression = ({
tumbleInterval: ACCELERATION_TIME_INTERVAL[0].value,
});
+ const updateGroupByStates = (newGroupByValue: GroupByTumbleType) => {
+ setGroupByValues(newGroupByValue);
+ setAccelerationFormData(
+ producer((accData) => {
+ accData.materializedViewQueryData.groupByTumbleValue = newGroupByValue;
+ accData.formErrors.materializedViewError = validateMaterializedViewData(
+ accData.accelerationIndexType,
+ accData.materializedViewQueryData
+ );
+ })
+ );
+ };
+
const onChangeTumbleWindow = (e: React.ChangeEvent) => {
- setGroupByValues({ ...groupbyValues, tumbleWindow: +e.target.value });
+ const newGroupByValue = { ...groupbyValues, tumbleWindow: +e.target.value };
+ updateGroupByStates(newGroupByValue);
};
const onChangeTumbleInterval = (e: React.ChangeEvent) => {
- setGroupByValues({ ...groupbyValues, tumbleInterval: e.target.value });
+ const newGroupByValue = { ...groupbyValues, tumbleInterval: e.target.value };
+ updateGroupByStates(newGroupByValue);
};
const onChangeTimeField = (selectedOptions: EuiComboBoxOptionOption[]) => {
- if (selectedOptions.length > 0)
- setGroupByValues({ ...groupbyValues, timeField: selectedOptions[0].label });
+ if (selectedOptions.length > 0) {
+ const newGroupByValue = { ...groupbyValues, timeField: selectedOptions[0].label };
+ updateGroupByStates(newGroupByValue);
+ }
};
- useEffect(() => {
- setAccelerationFormData({
- ...accelerationFormData,
- materializedViewQueryData: {
- ...accelerationFormData.materializedViewQueryData,
- groupByTumbleValue: groupbyValues,
- },
- });
- }, [groupbyValues]);
-
return (
setIsGroupPopOverOpen(true)}
- isInvalid={groupbyValues.timeField === ''}
+ isInvalid={
+ hasError(accelerationFormData.formErrors, 'materializedViewError') &&
+ groupbyValues.timeField === ''
+ }
/>
}
isOpen={IsGroupPopOverOpen}
@@ -89,6 +100,7 @@ export const GroupByTumbleExpression = ({
.map((value) => ({ label: value.fieldName }))}
selectedOptions={[{ label: groupbyValues.timeField }]}
onChange={onChangeTimeField}
+ isClearable={false}
/>
diff --git a/public/components/acceleration/visual_editors/materialized_view/materialized_view_builder.tsx b/public/components/acceleration/visual_editors/materialized_view/materialized_view_builder.tsx
index 0f3bdeea..124b451c 100644
--- a/public/components/acceleration/visual_editors/materialized_view/materialized_view_builder.tsx
+++ b/public/components/acceleration/visual_editors/materialized_view/materialized_view_builder.tsx
@@ -11,9 +11,15 @@ import {
EuiText,
htmlIdGenerator,
} from '@elastic/eui';
+import producer from 'immer';
import _ from 'lodash';
import React, { useEffect, useState } from 'react';
-import { CreateAccelerationForm, MaterializedViewColumn } from '../../../../../common/types';
+import {
+ AggregationFunctionType,
+ CreateAccelerationForm,
+ MaterializedViewColumn,
+} from '../../../../../common/types';
+import { hasError, validateMaterializedViewData } from '../../create/utils';
import { AddColumnPopOver } from './add_column_popover';
import { ColumnExpression } from './column_expression';
import { GroupByTumbleExpression } from './group_by_tumble_expression';
@@ -36,27 +42,27 @@ export const MaterializedViewBuilder = ({
useEffect(() => {
if (accelerationFormData.dataTableFields.length > 0) {
- setColumnExpressionValues([
+ const newColumnExpresionValue = [
{
id: newColumnExpressionId,
- functionName: 'count',
+ functionName: 'count' as AggregationFunctionType,
functionParam: accelerationFormData.dataTableFields[0].fieldName,
fieldAlias: 'counter1',
},
- ]);
+ ];
+ setAccelerationFormData(
+ producer((accData) => {
+ accData.materializedViewQueryData.columnsValues = newColumnExpresionValue;
+ accData.formErrors.materializedViewError = validateMaterializedViewData(
+ accData.accelerationIndexType,
+ accData.materializedViewQueryData
+ );
+ })
+ );
+ setColumnExpressionValues(newColumnExpresionValue);
}
}, [accelerationFormData.dataTableFields]);
- useEffect(() => {
- setAccelerationFormData({
- ...accelerationFormData,
- materializedViewQueryData: {
- ...accelerationFormData.materializedViewQueryData,
- columnsValues: columnExpressionValues,
- },
- });
- }, [columnExpressionValues]);
-
return (
<>
@@ -74,7 +80,15 @@ export const MaterializedViewBuilder = ({
-
+
@@ -95,6 +110,7 @@ export const MaterializedViewBuilder = ({
columnExpressionValues={columnExpressionValues}
setColumnExpressionValues={setColumnExpressionValues}
accelerationFormData={accelerationFormData}
+ setAccelerationFormData={setAccelerationFormData}
/>
);
})}
diff --git a/public/plugin.ts b/public/plugin.ts
index 10836ae1..213f4142 100644
--- a/public/plugin.ts
+++ b/public/plugin.ts
@@ -4,8 +4,8 @@
*/
import { AppMountParameters, CoreSetup, CoreStart, Plugin } from '../../../src/core/public';
-import { WorkbenchPluginSetup, WorkbenchPluginStart, AppPluginStartDependencies } from './types';
import { PLUGIN_NAME } from '../common/constants';
+import { AppPluginStartDependencies, WorkbenchPluginSetup, WorkbenchPluginStart } from './types';
export class WorkbenchPlugin implements Plugin {
public setup(core: CoreSetup): WorkbenchPluginSetup {
diff --git a/test/mocks/accelerationMock.ts b/test/mocks/accelerationMock.ts
new file mode 100644
index 00000000..5356d610
--- /dev/null
+++ b/test/mocks/accelerationMock.ts
@@ -0,0 +1,76 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { SkippingIndexRowType, materializedViewQueryType } from '../../common/types';
+
+export const skippingIndexDataMock: SkippingIndexRowType[] = [
+ {
+ id: '1',
+ fieldName: 'field1',
+ dataType: 'string',
+ accelerationMethod: 'PARTITION',
+ },
+ {
+ id: '2',
+ fieldName: 'field2',
+ dataType: 'number',
+ accelerationMethod: 'VALUE_SET',
+ },
+];
+
+export const coveringIndexDataMock: string[] = ['field1', 'field2', 'field3'];
+
+export const materializedViewEmptyDataMock = {
+ columnsValues: [],
+ groupByTumbleValue: {
+ timeField: '',
+ tumbleWindow: 0,
+ tumbleInterval: '',
+ },
+};
+
+export const materializedViewEmptyTumbleDataMock: materializedViewQueryType = {
+ columnsValues: [
+ {
+ id: '1',
+ functionName: 'count',
+ functionParam: 'field1',
+ },
+ ],
+ groupByTumbleValue: {
+ timeField: '',
+ tumbleWindow: 0,
+ tumbleInterval: 'second',
+ },
+};
+
+export const materializedViewStaleDataMock: materializedViewQueryType = {
+ columnsValues: [],
+ groupByTumbleValue: {
+ timeField: 'timestamp',
+ tumbleWindow: 10,
+ tumbleInterval: 'hour',
+ },
+};
+
+export const materializedViewValidDataMock: materializedViewQueryType = {
+ columnsValues: [
+ {
+ id: '1',
+ functionName: 'count',
+ functionParam: 'field1',
+ },
+ {
+ id: '2',
+ functionName: 'sum',
+ functionParam: 'field2',
+ },
+ ],
+ groupByTumbleValue: {
+ timeField: 'timestamp',
+ tumbleWindow: 5,
+ tumbleInterval: 'hour',
+ },
+};