From b506714b5c282eb603f53d383c65309a01b017bb Mon Sep 17 00:00:00 2001 From: Shenoy Pratik Date: Tue, 3 Oct 2023 09:47:01 -0700 Subject: [PATCH] Update form validations and tests for acceleration (#133) * update form validaitons Signed-off-by: Shenoy Pratik * update snapshots, add skipping validator Signed-off-by: Shenoy Pratik * update sqlpage snapshot Signed-off-by: Shenoy Pratik * add tests for acceleration create Signed-off-by: Shenoy Pratik * update acceleration builder tests Signed-off-by: Shenoy Pratik * update PR comments Signed-off-by: Shenoy Pratik * adding fix for windows snapshots Signed-off-by: Shenoy Pratik * update snapshot files Signed-off-by: Shenoy Pratik --------- Signed-off-by: Shenoy Pratik --- common/constants/index.ts | 9 + common/types/index.ts | 21 +- package.json | 3 + public/ace-themes/sql_console.js | 7 +- .../Main/__snapshots__/main.test.tsx.snap | 686 ++--- public/components/Main/main.tsx | 151 +- public/components/SQLPage/SQLPage.test.tsx | 30 +- public/components/SQLPage/SQLPage.tsx | 204 +- .../__snapshots__/SQLPage.test.tsx.snap | 186 +- .../caution_banner_callout.test.tsx.snap | 39 + .../create_acceleration.test.tsx.snap | 2475 +++++++++++++++++ .../create_acceleration_header.test.tsx.snap | 58 + .../__tests__/caution_banner_callout.test.tsx | 28 + .../__tests__/create_acceleration.test.tsx | 38 + .../create_acceleration_header.test.tsx | 28 + .../create/__tests__/utils.test.tsx | 262 ++ .../create/create_acceleration.tsx | 100 +- .../components/acceleration/create/utils.tsx | 138 +- .../define_index_options.test.tsx.snap | 336 +++ .../index_setting_options.test.tsx.snap | 1154 ++++++++ .../index_type_selector.test.tsx.snap | 309 ++ .../source_selector.test.tsx.snap | 774 ++++++ .../__tests__/define_index_options.test.tsx | 85 + .../__tests__/index_setting_options.test.tsx | 89 + .../__tests__/index_type_selector.test.tsx | 60 + .../__tests__/source_selector.test.tsx | 62 + .../selectors/define_index_options.tsx | 21 +- .../selectors/index_setting_options.tsx | 94 +- .../selectors/index_type_selector.tsx | 20 +- .../selectors/source_selector.tsx | 50 +- .../query_visual_editor.test.tsx.snap | 1126 ++++++++ .../__tests__/query_builder.test.tsx | 84 + .../__tests__/query_visual_editor.test.tsx | 116 + .../covering_index_builder.test.tsx.snap | 191 ++ .../__tests__/covering_index_builder.test.tsx | 60 + .../covering_index/covering_index_builder.tsx | 6 +- .../add_column_popover.test.tsx.snap | 84 + .../column_expression.test.tsx.snap | 175 ++ .../group_by_tumble_expression.test.tsx.snap | 65 + .../materialized_view_builder.test.tsx.snap | 318 +++ .../__tests__/add_column_popover.test.tsx | 76 + .../__tests__/column_expression.test.tsx | 78 + .../group_by_tumble_expression.test.tsx | 64 + .../materialized_view_builder.test.tsx | 64 + .../materialized_view/add_column_popover.tsx | 51 +- .../materialized_view/column_expression.tsx | 31 +- .../group_by_tumble_expression.tsx | 46 +- .../materialized_view_builder.tsx | 46 +- .../visual_editors/query_builder.tsx | 12 +- .../add_fields_modal.test.tsx.snap | 1509 ++++++++++ .../delete_fields_modal.test.tsx.snap | 2194 +++++++++++++++ .../skipping_index_builder.test.tsx.snap | 855 ++++++ .../__tests__/add_fields_modal.test.tsx | 68 + .../__tests__/delete_fields_modal.test.tsx | 68 + .../__tests__/skipping_index_builder.test.tsx | 64 + .../skipping_index/add_fields_modal.tsx | 32 +- .../skipping_index/delete_fields_modal.tsx | 2 +- .../skipping_index/skipping_index_builder.tsx | 42 +- public/plugin.ts | 2 +- test/jest.config.js | 56 +- test/mocks/accelerationMock.ts | 378 +++ yarn.lock | 854 +++--- 62 files changed, 15135 insertions(+), 1199 deletions(-) create mode 100644 public/components/acceleration/create/__tests__/__snapshots__/caution_banner_callout.test.tsx.snap create mode 100644 public/components/acceleration/create/__tests__/__snapshots__/create_acceleration.test.tsx.snap create mode 100644 public/components/acceleration/create/__tests__/__snapshots__/create_acceleration_header.test.tsx.snap create mode 100644 public/components/acceleration/create/__tests__/caution_banner_callout.test.tsx create mode 100644 public/components/acceleration/create/__tests__/create_acceleration.test.tsx create mode 100644 public/components/acceleration/create/__tests__/create_acceleration_header.test.tsx create mode 100644 public/components/acceleration/create/__tests__/utils.test.tsx create mode 100644 public/components/acceleration/selectors/__tests__/__snapshots__/define_index_options.test.tsx.snap create mode 100644 public/components/acceleration/selectors/__tests__/__snapshots__/index_setting_options.test.tsx.snap create mode 100644 public/components/acceleration/selectors/__tests__/__snapshots__/index_type_selector.test.tsx.snap create mode 100644 public/components/acceleration/selectors/__tests__/__snapshots__/source_selector.test.tsx.snap create mode 100644 public/components/acceleration/selectors/__tests__/define_index_options.test.tsx create mode 100644 public/components/acceleration/selectors/__tests__/index_setting_options.test.tsx create mode 100644 public/components/acceleration/selectors/__tests__/index_type_selector.test.tsx create mode 100644 public/components/acceleration/selectors/__tests__/source_selector.test.tsx create mode 100644 public/components/acceleration/visual_editors/__tests__/__snapshots__/query_visual_editor.test.tsx.snap create mode 100644 public/components/acceleration/visual_editors/__tests__/query_builder.test.tsx create mode 100644 public/components/acceleration/visual_editors/__tests__/query_visual_editor.test.tsx create mode 100644 public/components/acceleration/visual_editors/covering_index/__tests__/__snapshots__/covering_index_builder.test.tsx.snap create mode 100644 public/components/acceleration/visual_editors/covering_index/__tests__/covering_index_builder.test.tsx create mode 100644 public/components/acceleration/visual_editors/materialized_view/__tests__/__snapshots__/add_column_popover.test.tsx.snap create mode 100644 public/components/acceleration/visual_editors/materialized_view/__tests__/__snapshots__/column_expression.test.tsx.snap create mode 100644 public/components/acceleration/visual_editors/materialized_view/__tests__/__snapshots__/group_by_tumble_expression.test.tsx.snap create mode 100644 public/components/acceleration/visual_editors/materialized_view/__tests__/__snapshots__/materialized_view_builder.test.tsx.snap create mode 100644 public/components/acceleration/visual_editors/materialized_view/__tests__/add_column_popover.test.tsx create mode 100644 public/components/acceleration/visual_editors/materialized_view/__tests__/column_expression.test.tsx create mode 100644 public/components/acceleration/visual_editors/materialized_view/__tests__/group_by_tumble_expression.test.tsx create mode 100644 public/components/acceleration/visual_editors/materialized_view/__tests__/materialized_view_builder.test.tsx create mode 100644 public/components/acceleration/visual_editors/skipping_index/__tests__/__snapshots__/add_fields_modal.test.tsx.snap create mode 100644 public/components/acceleration/visual_editors/skipping_index/__tests__/__snapshots__/delete_fields_modal.test.tsx.snap create mode 100644 public/components/acceleration/visual_editors/skipping_index/__tests__/__snapshots__/skipping_index_builder.test.tsx.snap create mode 100644 public/components/acceleration/visual_editors/skipping_index/__tests__/add_fields_modal.test.tsx create mode 100644 public/components/acceleration/visual_editors/skipping_index/__tests__/delete_fields_modal.test.tsx create mode 100644 public/components/acceleration/visual_editors/skipping_index/__tests__/skipping_index_builder.test.tsx create mode 100644 test/mocks/accelerationMock.ts diff --git a/common/constants/index.ts b/common/constants/index.ts index e43eed0e..8533039d 100644 --- a/common/constants/index.ts +++ b/common/constants/index.ts @@ -32,7 +32,16 @@ export const ACCELERATION_TIME_INTERVAL = [ { text: 'week(s)', value: 'week' }, ]; +export const SKIPPING_INDEX_ACCELERATION_METHODS = [ + { value: 'PARTITION', text: 'Partition' }, + { value: 'VALUE_SET', text: 'Value Set' }, + { value: 'MIN_MAX', text: 'Min Max' }, +]; + export const ACCELERATION_ADD_FIELDS_TEXT = '(add fields here)'; +export const ACCELERATION_INDEX_NAME_REGEX = /^[a-z][a-z_\-]*$/; +export const ACCELERATION_S3_URL_REGEX = /^(s3|s3a):\/\/[a-zA-Z0-9.\-]+\/.*/; +export const ACCELERATION_DEFUALT_SKIPPING_INDEX_NAME = 'skipping'; export const ACCELERATION_INDEX_NAME_INFO = `All OpenSearch acceleration indices have a naming format of pattern: \`prefix__suffix\`. They share a common prefix structure, which is \`flint____\`. Additionally, they may have a suffix that varies based on the index type. ##### Skipping Index diff --git a/common/types/index.ts b/common/types/index.ts index ed1783a9..928d7578 100644 --- a/common/types/index.ts +++ b/common/types/index.ts @@ -12,11 +12,13 @@ export interface MaterializedViewColumn { fieldAlias?: string; } +export type SkippingIndexAccMethodType = 'PARTITION' | 'VALUE_SET' | 'MIN_MAX'; + export interface SkippingIndexRowType { id: string; fieldName: string; dataType: string; - accelerationMethod: 'PARTITION' | 'VALUE_SET' | 'MIN_MAX'; + accelerationMethod: SkippingIndexAccMethodType; } export interface DataTableFieldsType { @@ -43,6 +45,20 @@ export interface materializedViewQueryType { groupByTumbleValue: GroupByTumbleType; } +export interface FormErrorsType { + dataSourceError: string[]; + databaseError: string[]; + dataTableError: string[]; + skippingIndexError: string[]; + coveringIndexError: string[]; + materializedViewError: string[]; + indexNameError: string[]; + primaryShardsError: string[]; + replicaShardsError: string[]; + refreshIntervalError: string[]; + checkpointLocationError: string[]; +} + export interface CreateAccelerationForm { dataSource: string; database: string; @@ -57,5 +73,6 @@ export interface CreateAccelerationForm { replicaShardsCount: number; refreshType: 'interval' | 'auto'; checkpointLocation: string | undefined; - refreshIntervalOptions: RefreshIntervalType | undefined; + refreshIntervalOptions: RefreshIntervalType; + formErrors: FormErrorsType; } 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/Main/__snapshots__/main.test.tsx.snap b/public/components/Main/__snapshots__/main.test.tsx.snap index 29d848bc..2f967b2b 100644 --- a/public/components/Main/__snapshots__/main.test.tsx.snap +++ b/public/components/Main/__snapshots__/main.test.tsx.snap @@ -406,61 +406,69 @@ exports[`
spec click clear button 1`] = ` class="euiSpacer euiSpacer--l" />
- -
-
- +
+
- - Clear - - - -
-
- +
+
- - Explain - - - + + + Explain + + + +
+
@@ -941,61 +949,69 @@ exports[`
spec click run button, and response causes an error 1`] = ` class="euiSpacer euiSpacer--l" />
- -
-
- +
+
- - Clear - - - -
-
- +
+
- - Explain - - - + + + Explain + + + +
+
@@ -1476,61 +1492,69 @@ exports[`
spec click run button, and response is not ok 1`] = ` class="euiSpacer euiSpacer--l" />
- -
-
- +
+
- - Clear - - - -
-
- +
+
- - Explain - - - + + + Explain + + + +
+
@@ -2077,61 +2101,69 @@ exports[`
spec click run button, and response is ok 1`] = ` class="euiSpacer euiSpacer--l" />
- -
-
- +
+
- - Clear - - - -
-
- +
+
- - Explain - - - + + + Explain + + + +
+
@@ -2674,61 +2706,69 @@ exports[`
spec click run button, response fills null and missing values class="euiSpacer euiSpacer--l" />
- -
-
- +
+
- - Clear - - - -
-
- +
+
- - Explain - - - + + + Explain + + + +
+
@@ -3212,61 +3252,69 @@ exports[`
spec click translation button, and response is ok 1`] = ` class="euiSpacer euiSpacer--l" />
- -
-
- +
+
- - Clear - - - -
-
- +
+
- - Explain - - - + + + Explain + + + +
+
@@ -3757,61 +3805,69 @@ exports[`
spec renders the component 1`] = ` class="euiSpacer euiSpacer--l" />
- -
-
- +
+
- - Clear - - - -
-
- +
+
- - Explain - - - + + + Explain + + + +
+
diff --git a/public/components/Main/main.tsx b/public/components/Main/main.tsx index a80bdd41..e9f30cbc 100644 --- a/public/components/Main/main.tsx +++ b/public/components/Main/main.tsx @@ -3,18 +3,18 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { - EuiButton, - EuiFlexGroup, +import { + EuiButton, + EuiComboBox, + EuiFlexGroup, EuiFlexItem, - EuiSpacer, - EuiPageSideBar , - EuiPanel, - EuiPage, + EuiPage, EuiPageContent, - EuiPageContentBody, - EuiComboBox, - EuiText, + EuiPageContentBody, + EuiPageSideBar, + EuiPanel, + EuiSpacer, + EuiText, } from '@elastic/eui'; import { IHttpResponse } from 'angular'; import _ from 'lodash'; @@ -22,17 +22,17 @@ import React from 'react'; import { ChromeBreadcrumb, CoreStart } from '../../../../../src/core/public'; import { MESSAGE_TAB_LABEL } from '../../utils/constants'; import { + Tree, getDefaultTabId, getDefaultTabLabel, getQueries, getSelectedResults, - Tree, } from '../../utils/utils'; import { PPLPage } from '../PPLPage/PPLPage'; import Switch from '../QueryLanguageSwitch/Switch'; import QueryResults from '../QueryResults/QueryResults'; import { SQLPage } from '../SQLPage/SQLPage'; -import { TableView } from '../SQLPage/TableView' +import { TableView } from '../SQLPage/TableView'; interface ResponseData { ok: boolean; @@ -217,7 +217,7 @@ export class Main extends React.Component { this.onChange = this.onChange.bind(this); this.state = { language: 'SQL', - sqlQueriesString: 'SHOW tables LIKE \'%\';', + sqlQueriesString: "SHOW tables LIKE '%';", pplQueriesString: '', queries: [], queryTranslations: [], @@ -232,7 +232,7 @@ export class Main extends React.Component { itemIdToExpandedRowMap: {}, messages: [], isResultFullScreen: false, - selectedDatasource: '' + selectedDatasource: '', }; this.httpClient = this.props.httpClient; this.updateSQLQueries = _.debounce(this.updateSQLQueries, 250).bind(this); @@ -607,11 +607,11 @@ export class Main extends React.Component { ); // added callback function to handle async issues }; - updateSQLQueries(query: string) { + updateSQLQueries = (query: string) => { this.setState({ sqlQueriesString: query, }); - } + }; updatePPLQueries(query: string) { this.setState({ @@ -626,11 +626,11 @@ export class Main extends React.Component { } handleComboOptionChange = (selectedOption: string) => { - this.setState({ - selectedDatasource: selectedOption }); + this.setState({ + selectedDatasource: selectedOption, + }); }; - render() { let page; let link; @@ -645,6 +645,7 @@ export class Main extends React.Component { sqlQuery={this.state.sqlQueriesString} sqlTranslations={this.state.queryTranslations} updateSQLQueries={this.updateSQLQueries} + selectedDatasource={this.state.selectedDatasource} /> ); link = 'https://opensearch.org/docs/latest/search-plugins/sql/index/'; @@ -706,17 +707,19 @@ export class Main extends React.Component { return ( <> - + Data Sources { const selectedValue = selectedOptions[0] ? selectedOptions[0].value : ''; this.handleComboOptionChange(selectedValue); @@ -733,26 +736,22 @@ export class Main extends React.Component { - + {this.state.language === 'SQL' && ( - + Create - + /> @@ -760,56 +759,54 @@ export class Main extends React.Component { )} - + - - +
{page}
- -
- -
-
-
-
+ +
+ +
+ + +
- ); } } diff --git a/public/components/SQLPage/SQLPage.test.tsx b/public/components/SQLPage/SQLPage.test.tsx index 7b08b5d8..d1e7ccc3 100644 --- a/public/components/SQLPage/SQLPage.test.tsx +++ b/public/components/SQLPage/SQLPage.test.tsx @@ -3,24 +3,22 @@ * SPDX-License-Identifier: Apache-2.0 */ +import '@testing-library/jest-dom/extend-expect'; +import { fireEvent, render } from '@testing-library/react'; +import React from 'react'; +import { SQLPage } from './SQLPage'; -import React from "react"; -import "@testing-library/jest-dom/extend-expect"; -import { render, fireEvent } from "@testing-library/react"; -import { SQLPage } from "./SQLPage"; - - -describe(" spec", () => { - - it("renders the component", () => { +describe(' spec', () => { + it('renders the component', () => { render( { }} - onTranslate={() => { }} - onClear={() => { }} - updateSQLQueries={() => { }} + onRun={() => {}} + onTranslate={() => {}} + onClear={() => {}} + updateSQLQueries={() => {}} sqlTranslations={[]} sqlQuery={''} + selectedDatasource={''} /> ); expect(document.body.children[0]).toMatchSnapshot(); @@ -40,6 +38,7 @@ describe(" spec", () => { updateSQLQueries={updateSQLQueries} sqlTranslations={[]} sqlQuery={''} + selectedDatasource={'S3'} /> ); @@ -53,10 +52,5 @@ describe(" spec", () => { fireEvent.click(getByText('Explain')); expect(onTranslate).toHaveBeenCalledTimes(1); - }); - }); - - - diff --git a/public/components/SQLPage/SQLPage.tsx b/public/components/SQLPage/SQLPage.tsx index 94808e17..0cc0bac4 100644 --- a/public/components/SQLPage/SQLPage.tsx +++ b/public/components/SQLPage/SQLPage.tsx @@ -3,43 +3,43 @@ * SPDX-License-Identifier: Apache-2.0 */ - -import React from "react"; import { - EuiPanel, EuiButton, + EuiCodeBlock, + EuiCodeEditor, EuiFlexGroup, EuiFlexItem, - EuiOverlayMask, EuiModal, - EuiModalHeader, - EuiModalHeaderTitle, EuiModalBody, EuiModalFooter, - EuiCodeBlock, - EuiText, - EuiCodeEditor, + EuiModalHeader, + EuiModalHeaderTitle, + EuiOverlayMask, + EuiPanel, EuiSpacer, -} from "@elastic/eui"; -import { ResponseDetail, TranslateResult } from '../Main/main'; -import _ from 'lodash'; -import "brace/mode/sql"; -import "../../ace-themes/sql_console"; +} from '@elastic/eui'; import 'brace/ext/language_tools'; +import 'brace/mode/sql'; +import React from 'react'; +import '../../ace-themes/sql_console'; +import { ResponseDetail, TranslateResult } from '../Main/main'; +import { CreateAcceleration } from '../acceleration/create/create_acceleration'; interface SQLPageProps { - onRun: (query: string) => void, - onTranslate: (query: string) => void, - onClear: () => void, - updateSQLQueries: (query: string) => void - sqlQuery: string, - sqlTranslations: ResponseDetail[] + onRun: (query: string) => void; + onTranslate: (query: string) => void; + onClear: () => void; + updateSQLQueries: (query: string) => void; + sqlQuery: string; + sqlTranslations: ResponseDetail[]; + selectedDatasource: string; } interface SQLPageState { - sqlQuery: string, - translation: string, - isModalVisible: boolean + sqlQuery: string; + translation: string; + isModalVisible: boolean; + flyoutComponent: JSX.Element; } export class SQLPage extends React.Component { @@ -47,19 +47,35 @@ export class SQLPage extends React.Component { super(props); this.state = { sqlQuery: this.props.sqlQuery, - translation: "", - isModalVisible: false + translation: '', + isModalVisible: false, + flyoutComponent: <>, }; } setIsModalVisible(visible: boolean): void { this.setState({ - isModalVisible: visible - }) + isModalVisible: visible, + }); } - render() { + resetFlyout = () => { + this.setState({ flyoutComponent: <> }); + }; + setAccelerationFlyout = () => { + this.setState({ + flyoutComponent: ( + + ), + }); + }; + + render() { const closeModal = () => this.setIsModalVisible(false); const showModal = () => this.setIsModalVisible(true); @@ -68,11 +84,13 @@ export class SQLPage extends React.Component { return this.props.sqlTranslations[0].fulfilled; } return false; - } + }; const explainContent = sqlTranslationsNotEmpty() - ? this.props.sqlTranslations.map((queryTranslation: any) => JSON.stringify(queryTranslation.data, null, 2)).join("\n") - : 'This query is not explainable.'; + ? this.props.sqlTranslations + .map((queryTranslation: any) => JSON.stringify(queryTranslation.data, null, 2)) + .join('\n') + : 'This query is not explainable.'; let modal; @@ -85,11 +103,7 @@ export class SQLPage extends React.Component { - + {explainContent} @@ -97,7 +111,7 @@ export class SQLPage extends React.Component { Close - + @@ -105,60 +119,68 @@ export class SQLPage extends React.Component { } return ( - - - - - - this.props.onRun(this.props.sqlQuery)} - > - - Run - - - { - this.props.updateSQLQueries(""); - this.props.onClear(); + <> + + + - - Clear - - - - this.props.onTranslate(this.props.sqlQuery) - } - > - - Explain - - {modal} - - - - ) + aria-label="Code Editor" + /> + + + + + this.props.onRun(this.props.sqlQuery)}> + + Run + + + { + this.props.updateSQLQueries(''); + this.props.onClear(); + }} + > + Clear + + this.props.onTranslate(this.props.sqlQuery)} + > + + Explain + + + + + {this.props.selectedDatasource === 'S3' && ( + + + Accelerate Table + + + )} + + + {modal} + {this.state.flyoutComponent} + + ); } } diff --git a/public/components/SQLPage/__snapshots__/SQLPage.test.tsx.snap b/public/components/SQLPage/__snapshots__/SQLPage.test.tsx.snap index d2fe7008..96850ed5 100644 --- a/public/components/SQLPage/__snapshots__/SQLPage.test.tsx.snap +++ b/public/components/SQLPage/__snapshots__/SQLPage.test.tsx.snap @@ -124,61 +124,69 @@ exports[` spec renders the component 1`] = ` class="euiSpacer euiSpacer--l" />
- -
-
- +
+
- - Clear - - - -
-
- +
+
- - Explain - - - + + + Explain + + + +
+
@@ -309,49 +317,75 @@ exports[` spec tests the action buttons 1`] = ` class="euiSpacer euiSpacer--l" />
- -
-
- +
+
- - Clear - - - + + + Clear + + + +
+
+ +
+
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..f40a09f9 --- /dev/null +++ b/public/components/acceleration/create/__tests__/__snapshots__/caution_banner_callout.test.tsx.snap @@ -0,0 +1,39 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Acceleration callout renders acceleration flyout callout 1`] = ` +
+
+ + + Considerations for data indexing + +
+
+
+

+ Warning about not indexing personal or sensitive data, something about the cost of indexing. +

+
+
+
+`; 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..3eded361 --- /dev/null +++ b/public/components/acceleration/create/__tests__/__snapshots__/create_acceleration.test.tsx.snap @@ -0,0 +1,2475 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Create acceleration flyout components renders acceleration flyout component with default options 1`] = ` +Array [ + "", + +
+ + + + + + + + + + + + + +
+
+ + + Field name + + + + + + Datatype + + + + + + Acceleration method + + + + + + Delete + + +
+
+ + No items found + +
+
+ + +
+
+ +
+
+ +
+
+ + + + +
+
+
+ +
+
+ +
+
+
+ + + + } + > + Array [ + Array [ +
, + "", +
+
+ +
+
+
+
+

+ Accelerate data +

+
+
+
+
+
+ Create OpenSearch Indexes from external data connections for better performance. + + + Learn more + + + (opens in a new tab or window) + + +
+
+
+
+
+
+
+
+
+ + + Considerations for data indexing + +
+
+
+

+ Warning about not indexing personal or sensitive data, something about the cost of indexing. +

+
+
+
+
+
+ Array [ +
+

+ Select data source +

+
, +
, +
+
+ Select data connection where the data you want to accelerate resides. +
+
, +
, +
+
+ +
+
+
+
+
+
+

+ Select a data source +

+
+ +
+
+
+
+ +
+
+
+
+
+ A data source has to be configured and active to be able to select it and index data from. +
+
+
, +
+
+ +
+
+
+
+
+
+

+ Select a database +

+
+ +
+
+
+
+ +
+
+
+
+
+ Select the database that contains the tables you'd like to use. +
+
+
, +
+
+ +
+
+
+
+
+
+

+ Select a table +

+
+ +
+
+
+
+ +
+
+
+
+
+ Select the Spark table that has the data you would like to index. +
+
+
, + ] +
+ Array [ +
+

+ Index settings +

+
, +
, +
+ +
+
+
+
+
+ + Skipping Index + +
+ +
+
+
+
+ +
+
+
+
+
+ Select the type of index you want to create. Each index type has benefits and costs. +
+
+
, +
+
+ +
+
+
+
+ +
+
+
+ Specify the number of primary shards for the index. Default is 5. The number of primary shards cannot be changed after the index is created. +
+
+
, +
+
+ +
+
+
+
+ +
+
+
+ Specify the number of replicas each primary shard should have. Default is 1. +
+
+
, +
+
+ +
+
+
+
+ +
+ +
+
+ +
+ +
+
+
+ Specify how often the index should refresh, which publishes the most recent changes and make them available for search. Default is set to auto refresh when data at the source changes. +
+
+
, +
+
+ +
+
+
+
+ +
+
+
+ The HDFS compatible file system location path for incremental refresh job checkpoint. +
+
+
, + ] +
+ Array [ +
+

+ Index settings +

+
, +
, +
+
+ + +
+ +
+
+
+
+ + + + +
+ +
+ +
+
+ Must be in lowercase letters. Cannot begin with underscores or hyphens. Spaces, commas, and characters :, ", *, +, /, \\, |, ?, #, >, or < are not allowed. Prefix and suffix are added to the name of generated OpenSearch index. +
+
+
, + "", + ] +
+ Array [ +
, + Array [ +
+

+ Skipping index definition +

+
, +
, +
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + +
+
+ + + Field name + + + + + + Datatype + + + + + + Acceleration method + + + + + + Delete + + +
+
+ + No items found + +
+
+
+
, +
+
+ +
+
+ +
+
, + ], + ] +
+
+
+
+
+
+
+ +
+
+ +
+
+
+
+
, +
, + ], + "", + ] + , +] +`; 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..1c099f4b --- /dev/null +++ b/public/components/acceleration/create/__tests__/__snapshots__/create_acceleration_header.test.tsx.snap @@ -0,0 +1,58 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Acceleration header renders acceleration flyout header 1`] = ` +
+
+
+

+ Accelerate data +

+
+
+
+
+
+ Create OpenSearch Indexes from external data connections for better performance. + + + Learn more + + + (opens in a new tab or window) + + +
+
+
+`; 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..4ad06632 --- /dev/null +++ b/public/components/acceleration/create/__tests__/caution_banner_callout.test.tsx @@ -0,0 +1,28 @@ +/* + * 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 toJson from 'enzyme-to-json'; +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(); + wrapper.update(); + await waitFor(() => { + expect( + toJson(wrapper, { + noKey: false, + mode: 'deep', + }) + ).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..8736c5b0 --- /dev/null +++ b/public/components/acceleration/create/__tests__/create_acceleration.test.tsx @@ -0,0 +1,38 @@ +/* + * 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 toJson from 'enzyme-to-json'; +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( + + ); + wrapper.update(); + await waitFor(() => { + expect( + toJson(wrapper, { + noKey: false, + mode: 'deep', + }) + ).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..8f2c42a1 --- /dev/null +++ b/public/components/acceleration/create/__tests__/create_acceleration_header.test.tsx @@ -0,0 +1,28 @@ +/* + * 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 toJson from 'enzyme-to-json'; +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(); + wrapper.update(); + await waitFor(() => { + expect( + toJson(wrapper, { + noKey: false, + mode: 'deep', + }) + ).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 e726adf0..4cfc5e44 100644 --- a/public/components/acceleration/create/create_acceleration.tsx +++ b/public/components/acceleration/create/create_acceleration.tsx @@ -12,11 +12,15 @@ import { EuiFlyoutBody, EuiFlyoutFooter, EuiFlyoutHeader, + EuiForm, EuiSpacer, } from '@elastic/eui'; import React, { useState } from 'react'; -import { ACCELERATION_TIME_INTERVAL } from '../../../../common/constants'; -import { CreateAccelerationForm } from '../../../../common/types/'; +import { + ACCELERATION_DEFUALT_SKIPPING_INDEX_NAME, + ACCELERATION_TIME_INTERVAL, +} from '../../../../common/constants'; +import { CreateAccelerationForm } from '../../../../common/types'; import { DefineIndexOptions } from '../selectors/define_index_options'; import { IndexSettingOptions } from '../selectors/index_setting_options'; import { AccelerationDataSourceSelector } from '../selectors/source_selector'; @@ -24,16 +28,17 @@ import { accelerationQueryBuilder } from '../visual_editors/query_builder'; import { QueryVisualEditor } from '../visual_editors/query_visual_editor'; import { CautionBannerCallout } from './caution_banner_callout'; import { CreateAccelerationHeader } from './create_acceleration_header'; +import { formValidator, hasError } from './utils'; export interface CreateAccelerationProps { dataSource: string; - setIsFlyoutVisible(visible: boolean): void; + resetFlyout: () => void; updateQueries: (query: string) => void; } export const CreateAcceleration = ({ dataSource, - setIsFlyoutVisible, + resetFlyout, updateQueries, }: CreateAccelerationProps) => { const [accelerationFormData, setAccelerationFormData] = useState({ @@ -52,7 +57,7 @@ export const CreateAcceleration = ({ tumbleInterval: '', }, }, - accelerationIndexName: '', + accelerationIndexName: ACCELERATION_DEFUALT_SKIPPING_INDEX_NAME, primaryShardsCount: 5, replicaShardsCount: 1, refreshType: 'auto', @@ -61,65 +66,76 @@ export const CreateAcceleration = ({ refreshWindow: 1, refreshInterval: ACCELERATION_TIME_INTERVAL[1].value, }, + formErrors: { + dataSourceError: [], + databaseError: [], + dataTableError: [], + skippingIndexError: [], + coveringIndexError: [], + materializedViewError: [], + indexNameError: [], + primaryShardsError: [], + replicaShardsError: [], + refreshIntervalError: [], + checkpointLocationError: [], + }, }); const copyToEditor = () => { + const errors = formValidator(accelerationFormData); + if (hasError(errors)) { + setAccelerationFormData({ ...accelerationFormData, formErrors: errors }); + return; + } updateQueries(accelerationQueryBuilder(accelerationFormData)); + resetFlyout(); }; return ( <> - setIsFlyoutVisible(false)} - aria-labelledby="flyoutTitle" - size="m" - > + - - - - - - - + + + + + + + + + - setIsFlyoutVisible(false)} - flush="left" - > + Close - { - copyToEditor(); - setIsFlyoutVisible(false); - }} - fill - > + Copy Query to Editor diff --git a/public/components/acceleration/create/utils.tsx b/public/components/acceleration/create/utils.tsx index 798a94ae..a293298c 100644 --- a/public/components/acceleration/create/utils.tsx +++ b/public/components/acceleration/create/utils.tsx @@ -3,13 +3,147 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ACCELERATION_INDEX_NAME_REGEX } from '../../../../common/constants'; +import { + ACCELERATION_INDEX_NAME_REGEX, + ACCELERATION_S3_URL_REGEX, +} from '../../../../common/constants'; +import { + AccelerationIndexType, + CreateAccelerationForm, + FormErrorsType, + SkippingIndexRowType, + materializedViewQueryType, +} from '../../../../common/types'; export const pluralizeTime = (timeWindow: number) => { return timeWindow > 1 ? 's' : ''; }; +export const hasError = (formErrors: FormErrorsType, key?: keyof FormErrorsType) => { + if (!key) return Object.values(formErrors).some((e) => !!e.length); + return !!formErrors[key]?.length; +}; + +export const validateDataSource = (dataSource: string) => { + return dataSource.trim().length === 0 ? ['Select a valid data source'] : []; +}; + +export const validateDatabase = (database: string) => { + return database.trim().length === 0 ? ['Select a valid database'] : []; +}; + +export const validateDataTable = (dataTable: string) => { + return dataTable.trim().length === 0 ? ['Select a valid table'] : []; +}; + +export const validatePrimaryShardCount = (primaryShardCount: number) => { + return primaryShardCount < 1 ? ['Primary shards count should be greater than 0'] : []; +}; + +export const validateReplicaCount = (replicaCount: number) => { + return replicaCount < 1 ? ['Replica count should be greater than 0'] : []; +}; + +export const validateRefreshInterval = (refreshType: string, refreshWindow: number) => { + return refreshType === 'interval' && refreshWindow < 1 + ? ['refresh window should be greater than 0'] + : []; +}; + export const validateIndexName = (value: string) => { // Check if the value does not begin with underscores or hyphens and all characters are lower case - return ACCELERATION_INDEX_NAME_REGEX.test(value); + return !ACCELERATION_INDEX_NAME_REGEX.test(value) ? ['Enter a valid index name'] : []; +}; + +export const validateCheckpointLocation = ( + accelerationIndexType: AccelerationIndexType, + checkpointLocation: string | undefined +) => { + if (accelerationIndexType === 'materialized' && !checkpointLocation) { + return ['Checkpoint location is mandatory for materialized view creation']; + } + + if (checkpointLocation && !ACCELERATION_S3_URL_REGEX.test(checkpointLocation)) + return ['Enter a valid checkpoint location']; + + return []; +}; + +export const validateSkippingIndexData = ( + accelerationIndexType: AccelerationIndexType, + skippingIndexQueryData: SkippingIndexRowType[] +) => { + // TODO: Validate dataType match with supported acceleration method type + if (accelerationIndexType !== 'skipping') return []; + + if (skippingIndexQueryData.length < 1) return ['Add fields to the skipping index definition']; + + return []; +}; + +export const validateCoveringIndexData = ( + accelerationIndexType: AccelerationIndexType, + coveringIndexQueryData: string[] +) => { + if (accelerationIndexType !== 'covering') return []; + + if (coveringIndexQueryData.length < 1) return ['Add fields to covering index definition']; + return []; +}; + +export const validateMaterializedViewData = ( + accelerationIndexType: AccelerationIndexType, + materializedViewQueryData: materializedViewQueryType +) => { + if (accelerationIndexType !== 'materialized') return []; + + if (materializedViewQueryData.columnsValues.length < 1) + return ['Add columns to materialized view definition']; + + if (materializedViewQueryData.groupByTumbleValue.timeField === '') + return ['Add a time field to tumble function in materialized view definition']; + + if (materializedViewQueryData.groupByTumbleValue.tumbleWindow < 1) + return ['Add a valid time window to tumble function in materialized view definition']; + return []; +}; + +export const formValidator = (accelerationformData: CreateAccelerationForm) => { + const { + dataSource, + database, + dataTable, + accelerationIndexType, + skippingIndexQueryData, + coveringIndexQueryData, + materializedViewQueryData, + accelerationIndexName, + primaryShardsCount, + replicaShardsCount, + refreshType, + checkpointLocation, + refreshIntervalOptions, + } = accelerationformData; + + const accelerationFormErrors = { + dataSourceError: validateDataSource(dataSource), + databaseError: validateDatabase(database), + dataTableError: validateDataTable(dataTable), + primaryShardsError: validatePrimaryShardCount(primaryShardsCount), + replicaShardsError: validateReplicaCount(replicaShardsCount), + refreshIntervalError: validateRefreshInterval( + refreshType, + refreshIntervalOptions.refreshWindow + ), + checkpointLocationError: validateCheckpointLocation(accelerationIndexType, checkpointLocation), + indexNameError: validateIndexName(accelerationIndexName), + skippingIndexError: validateSkippingIndexData(accelerationIndexType, skippingIndexQueryData), + coveringIndexError: validateCoveringIndexData(accelerationIndexType, coveringIndexQueryData), + materializedViewError: validateMaterializedViewData( + accelerationIndexType, + materializedViewQueryData + ), + }; + + return accelerationFormErrors; }; diff --git a/public/components/acceleration/selectors/__tests__/__snapshots__/define_index_options.test.tsx.snap b/public/components/acceleration/selectors/__tests__/__snapshots__/define_index_options.test.tsx.snap new file mode 100644 index 00000000..2ff218f3 --- /dev/null +++ b/public/components/acceleration/selectors/__tests__/__snapshots__/define_index_options.test.tsx.snap @@ -0,0 +1,336 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Index options acceleration components renders acceleration index options with covering index options 1`] = ` +Array [ +
+

+ Index settings +

+
, +
, +
+
+ + +
+ +
+
+
+
+ + + + + + +
+ +
+ +
+
+ Must be in lowercase letters. Cannot begin with underscores or hyphens. Spaces, commas, and characters :, ", *, +, /, \\, |, ?, #, >, or < are not allowed. Prefix and suffix are added to the name of generated OpenSearch index. +
+
+
, + "", +] +`; + +exports[`Index options acceleration components renders acceleration index options with default options 1`] = ` +Array [ +
+

+ Index settings +

+
, +
, +
+
+ + +
+ +
+
+
+
+ + + + +
+ +
+ +
+
+ Must be in lowercase letters. Cannot begin with underscores or hyphens. Spaces, commas, and characters :, ", *, +, /, \\, |, ?, #, >, or < are not allowed. Prefix and suffix are added to the name of generated OpenSearch index. +
+
+
, + "", +] +`; + +exports[`Index options acceleration components renders acceleration index options with materialized index options 1`] = ` +Array [ +
+

+ Index settings +

+
, +
, +
+
+ + +
+ +
+
+
+
+ + + + + + +
+ +
+
+
+ Must be in lowercase letters. Cannot begin with underscores or hyphens. Spaces, commas, and characters :, ", *, +, /, \\, |, ?, #, >, or < are not allowed. Prefix and suffix are added to the name of generated OpenSearch index. +
+
+
, + "", +] +`; diff --git a/public/components/acceleration/selectors/__tests__/__snapshots__/index_setting_options.test.tsx.snap b/public/components/acceleration/selectors/__tests__/__snapshots__/index_setting_options.test.tsx.snap new file mode 100644 index 00000000..72dad5ea --- /dev/null +++ b/public/components/acceleration/selectors/__tests__/__snapshots__/index_setting_options.test.tsx.snap @@ -0,0 +1,1154 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Index settings acceleration components renders acceleration index settings with default options 1`] = ` +Array [ +
+

+ Index settings +

+
, +
, +
+ +
+
+
+
+
+ + Skipping Index + +
+ +
+
+
+
+ +
+
+
+
+
+ Select the type of index you want to create. Each index type has benefits and costs. +
+
+
, +
+
+ +
+
+
+
+ +
+
+
+ Specify the number of primary shards for the index. Default is 5. The number of primary shards cannot be changed after the index is created. +
+
+
, +
+
+ +
+
+
+
+ +
+
+
+ Specify the number of replicas each primary shard should have. Default is 1. +
+
+
, +
+
+ +
+
+
+
+ +
+ +
+
+ +
+ +
+
+
+ Specify how often the index should refresh, which publishes the most recent changes and make them available for search. Default is set to auto refresh when data at the source changes. +
+
+
, +
+
+ +
+
+
+
+ +
+
+
+ The HDFS compatible file system location path for incremental refresh job checkpoint. +
+
+
, +] +`; + +exports[`Index settings acceleration components renders acceleration index settings with different options1 1`] = ` +Array [ +
+

+ Index settings +

+
, +
, +
+ +
+
+
+
+
+ + Skipping Index + +
+ +
+
+
+
+ +
+
+
+
+
+ Select the type of index you want to create. Each index type has benefits and costs. +
+
+
, +
+
+ +
+
+
+
+ +
+
+
+ Specify the number of primary shards for the index. Default is 5. The number of primary shards cannot be changed after the index is created. +
+
+
, +
+
+ +
+
+
+
+ +
+
+
+ Specify the number of replicas each primary shard should have. Default is 1. +
+
+
, +
+
+ +
+
+
+
+ +
+ +
+
+ +
+ +
+
+
+ Specify how often the index should refresh, which publishes the most recent changes and make them available for search. Default is set to auto refresh when data at the source changes. +
+
+
, +
+
+ +
+
+
+
+ +
+
+
+ The HDFS compatible file system location path for incremental refresh job checkpoint. +
+
+
, +] +`; + +exports[`Index settings acceleration components renders acceleration index settings with different options2 1`] = ` +Array [ +
+

+ Index settings +

+
, +
, +
+ +
+
+
+
+
+ + Skipping Index + +
+ +
+
+
+
+ +
+
+
+
+
+ Select the type of index you want to create. Each index type has benefits and costs. +
+
+
, +
+
+ +
+
+
+
+ +
+
+
+ Specify the number of primary shards for the index. Default is 5. The number of primary shards cannot be changed after the index is created. +
+
+
, +
+
+ +
+
+
+
+ +
+
+
+ Specify the number of replicas each primary shard should have. Default is 1. +
+
+
, +
+
+ +
+
+
+
+ +
+ +
+
+ +
+ +
+
+
+ Specify how often the index should refresh, which publishes the most recent changes and make them available for search. Default is set to auto refresh when data at the source changes. +
+
+
, +
+
+ +
+
+
+
+ +
+
+
+ The HDFS compatible file system location path for incremental refresh job checkpoint. +
+
+
, +] +`; diff --git a/public/components/acceleration/selectors/__tests__/__snapshots__/index_type_selector.test.tsx.snap b/public/components/acceleration/selectors/__tests__/__snapshots__/index_type_selector.test.tsx.snap new file mode 100644 index 00000000..6a41fd85 --- /dev/null +++ b/public/components/acceleration/selectors/__tests__/__snapshots__/index_type_selector.test.tsx.snap @@ -0,0 +1,309 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Index type selector components renders type selector with default options 1`] = ` +
+ +
+
+
+
+
+ + Skipping Index + +
+ +
+
+
+
+ +
+
+
+
+
+ Select the type of index you want to create. Each index type has benefits and costs. +
+
+
+`; + +exports[`Index type selector components renders type selector with different options 1`] = ` +
+ +
+
+
+
+
+ + Skipping Index + +
+ +
+
+
+
+ +
+
+
+
+
+ Select the type of index you want to create. Each index type has benefits and costs. +
+
+
+`; diff --git a/public/components/acceleration/selectors/__tests__/__snapshots__/source_selector.test.tsx.snap b/public/components/acceleration/selectors/__tests__/__snapshots__/source_selector.test.tsx.snap new file mode 100644 index 00000000..bf2a7962 --- /dev/null +++ b/public/components/acceleration/selectors/__tests__/__snapshots__/source_selector.test.tsx.snap @@ -0,0 +1,774 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Source selector components renders source selector with default options 1`] = ` +Array [ +
+

+ Select data source +

+
, +
, +
+
+ Select data connection where the data you want to accelerate resides. +
+
, +
, +
+
+ +
+
+
+
+
+
+

+ Select a data source +

+
+ +
+
+
+
+ +
+
+
+
+
+ A data source has to be configured and active to be able to select it and index data from. +
+
+
, +
+
+ +
+
+
+
+
+
+

+ Select a database +

+
+ +
+
+
+
+ +
+
+
+
+
+ Select the database that contains the tables you'd like to use. +
+
+
, +
+
+ +
+
+
+
+
+
+

+ Select a table +

+
+ +
+
+
+
+ +
+
+
+
+
+ Select the Spark table that has the data you would like to index. +
+
+
, +] +`; + +exports[`Source selector components renders source selector with different options 1`] = ` +Array [ +
+

+ Select data source +

+
, +
, +
+
+ Select data connection where the data you want to accelerate resides. +
+
, +
, +
+
+ +
+
+
+
+
+
+

+ Select a data source +

+
+ +
+
+
+
+ +
+
+
+
+
+ A data source has to be configured and active to be able to select it and index data from. +
+
+
, +
+
+ +
+
+
+
+
+
+

+ Select a database +

+
+ +
+
+
+
+ +
+
+
+
+
+ Select the database that contains the tables you'd like to use. +
+
+
, +
+
+ +
+
+
+
+
+
+

+ Select a table +

+
+ +
+
+
+
+ +
+
+
+
+
+ Select the Spark table that has the data you would like to index. +
+
+
, +] +`; diff --git a/public/components/acceleration/selectors/__tests__/define_index_options.test.tsx b/public/components/acceleration/selectors/__tests__/define_index_options.test.tsx new file mode 100644 index 00000000..4d8cf515 --- /dev/null +++ b/public/components/acceleration/selectors/__tests__/define_index_options.test.tsx @@ -0,0 +1,85 @@ +/* + * 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 toJson from 'enzyme-to-json'; +import React from 'react'; +import { CreateAccelerationForm } from '../../../../../common/types'; +import { createAccelerationEmptyDataMock } from '../../../../../test/mocks/accelerationMock'; +import { DefineIndexOptions } from '../define_index_options'; + +describe('Index options acceleration components', () => { + configure({ adapter: new Adapter() }); + + it('renders acceleration index options with default options', async () => { + const accelerationFormData = createAccelerationEmptyDataMock; + const setAccelerationFormData = jest.fn(); + const wrapper = mount( + + ); + wrapper.update(); + await waitFor(() => { + expect( + toJson(wrapper, { + noKey: false, + mode: 'deep', + }) + ).toMatchSnapshot(); + }); + }); + + it('renders acceleration index options with covering index options', async () => { + const accelerationFormData: CreateAccelerationForm = { + ...createAccelerationEmptyDataMock, + accelerationIndexType: 'covering', + accelerationIndexName: 'covering-idx', + }; + const setAccelerationFormData = jest.fn(); + const wrapper = mount( + + ); + wrapper.update(); + await waitFor(() => { + expect( + toJson(wrapper, { + noKey: false, + mode: 'deep', + }) + ).toMatchSnapshot(); + }); + }); + + it('renders acceleration index options with materialized index options', async () => { + const accelerationFormData: CreateAccelerationForm = { + ...createAccelerationEmptyDataMock, + accelerationIndexType: 'materialized', + accelerationIndexName: 'mv_metrics', + }; + const setAccelerationFormData = jest.fn(); + const wrapper = mount( + + ); + wrapper.update(); + await waitFor(() => { + expect( + toJson(wrapper, { + noKey: false, + mode: 'deep', + }) + ).toMatchSnapshot(); + }); + }); +}); diff --git a/public/components/acceleration/selectors/__tests__/index_setting_options.test.tsx b/public/components/acceleration/selectors/__tests__/index_setting_options.test.tsx new file mode 100644 index 00000000..a8094b19 --- /dev/null +++ b/public/components/acceleration/selectors/__tests__/index_setting_options.test.tsx @@ -0,0 +1,89 @@ +/* + * 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 toJson from 'enzyme-to-json'; +import React from 'react'; +import { CreateAccelerationForm } from '../../../../../common/types'; +import { createAccelerationEmptyDataMock } from '../../../../../test/mocks/accelerationMock'; +import { IndexSettingOptions } from '../index_setting_options'; + +describe('Index settings acceleration components', () => { + configure({ adapter: new Adapter() }); + + it('renders acceleration index settings with default options', async () => { + const accelerationFormData = createAccelerationEmptyDataMock; + const setAccelerationFormData = jest.fn(); + const wrapper = mount( + + ); + wrapper.update(); + await waitFor(() => { + expect( + toJson(wrapper, { + noKey: false, + mode: 'deep', + }) + ).toMatchSnapshot(); + }); + }); + + it('renders acceleration index settings with different options1', async () => { + const accelerationFormData: CreateAccelerationForm = { + ...createAccelerationEmptyDataMock, + primaryShardsCount: 1, + replicaShardsCount: 5, + refreshType: 'auto', + }; + const setAccelerationFormData = jest.fn(); + const wrapper = mount( + + ); + wrapper.update(); + await waitFor(() => { + expect( + toJson(wrapper, { + noKey: false, + mode: 'deep', + }) + ).toMatchSnapshot(); + }); + }); + + it('renders acceleration index settings with different options2', async () => { + const accelerationFormData: CreateAccelerationForm = { + ...createAccelerationEmptyDataMock, + primaryShardsCount: 5, + replicaShardsCount: 1, + refreshType: 'interval', + refreshIntervalOptions: { refreshWindow: 1, refreshInterval: 'second' }, + checkpointLocation: 's3://test/url', + }; + const setAccelerationFormData = jest.fn(); + const wrapper = mount( + + ); + wrapper.update(); + await waitFor(() => { + expect( + toJson(wrapper, { + noKey: false, + mode: 'deep', + }) + ).toMatchSnapshot(); + }); + }); +}); diff --git a/public/components/acceleration/selectors/__tests__/index_type_selector.test.tsx b/public/components/acceleration/selectors/__tests__/index_type_selector.test.tsx new file mode 100644 index 00000000..ba2e082c --- /dev/null +++ b/public/components/acceleration/selectors/__tests__/index_type_selector.test.tsx @@ -0,0 +1,60 @@ +/* + * 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 toJson from 'enzyme-to-json'; +import React from 'react'; +import { CreateAccelerationForm } from '../../../../../common/types'; +import { createAccelerationEmptyDataMock } from '../../../../../test/mocks/accelerationMock'; +import { IndexTypeSelector } from '../index_type_selector'; + +describe('Index type selector components', () => { + configure({ adapter: new Adapter() }); + + it('renders type selector with default options', async () => { + const accelerationFormData = createAccelerationEmptyDataMock; + const setAccelerationFormData = jest.fn(); + const wrapper = mount( + + ); + wrapper.update(); + await waitFor(() => { + expect( + toJson(wrapper, { + noKey: false, + mode: 'deep', + }) + ).toMatchSnapshot(); + }); + }); + + it('renders type selector with different options', async () => { + const accelerationFormData: CreateAccelerationForm = { + ...createAccelerationEmptyDataMock, + accelerationIndexType: 'covering', + }; + const setAccelerationFormData = jest.fn(); + const wrapper = mount( + + ); + wrapper.update(); + await waitFor(() => { + expect( + toJson(wrapper, { + noKey: false, + mode: 'deep', + }) + ).toMatchSnapshot(); + }); + }); +}); diff --git a/public/components/acceleration/selectors/__tests__/source_selector.test.tsx b/public/components/acceleration/selectors/__tests__/source_selector.test.tsx new file mode 100644 index 00000000..f5d1ff57 --- /dev/null +++ b/public/components/acceleration/selectors/__tests__/source_selector.test.tsx @@ -0,0 +1,62 @@ +/* + * 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 toJson from 'enzyme-to-json'; +import React from 'react'; +import { CreateAccelerationForm } from '../../../../../common/types'; +import { createAccelerationEmptyDataMock } from '../../../../../test/mocks/accelerationMock'; +import { AccelerationDataSourceSelector } from '../source_selector'; + +describe('Source selector components', () => { + configure({ adapter: new Adapter() }); + + it('renders source selector with default options', async () => { + const accelerationFormData = createAccelerationEmptyDataMock; + const setAccelerationFormData = jest.fn(); + const wrapper = mount( + + ); + wrapper.update(); + await waitFor(() => { + expect( + toJson(wrapper, { + noKey: false, + mode: 'deep', + }) + ).toMatchSnapshot(); + }); + }); + + it('renders source selector with different options', async () => { + const accelerationFormData: CreateAccelerationForm = { + ...createAccelerationEmptyDataMock, + dataSource: 'ds', + database: 'db', + dataTable: 'tb', + }; + const setAccelerationFormData = jest.fn(); + const wrapper = mount( + + ); + wrapper.update(); + await waitFor(() => { + expect( + toJson(wrapper, { + noKey: false, + mode: 'deep', + }) + ).toMatchSnapshot(); + }); + }); +}); diff --git a/public/components/acceleration/selectors/define_index_options.tsx b/public/components/acceleration/selectors/define_index_options.tsx index e43f7258..9deebd87 100644 --- a/public/components/acceleration/selectors/define_index_options.tsx +++ b/public/components/acceleration/selectors/define_index_options.tsx @@ -20,10 +20,11 @@ import { EuiSpacer, EuiText, } from '@elastic/eui'; +import producer from 'immer'; import React, { ChangeEvent, useState } from 'react'; import { ACCELERATION_INDEX_NAME_INFO } from '../../../../common/constants'; import { CreateAccelerationForm } from '../../../../common/types'; -import { validateIndexName } from '../create/utils'; +import { hasError, validateIndexName } from '../create/utils'; interface DefineIndexOptionsProps { accelerationFormData: CreateAccelerationForm; @@ -34,7 +35,6 @@ export const DefineIndexOptions = ({ accelerationFormData, setAccelerationFormData, }: DefineIndexOptionsProps) => { - const [indexName, setIndexName] = useState(''); const [modalComponent, setModalComponent] = useState(<>); const modalValue = ( @@ -61,7 +61,6 @@ export const DefineIndexOptions = ({ const onChangeIndexName = (e: ChangeEvent) => { setAccelerationFormData({ ...accelerationFormData, accelerationIndexName: e.target.value }); - setIndexName(e.target.value); }; const getPreprend = () => { @@ -94,8 +93,9 @@ export const DefineIndexOptions = ({ setModalComponent(modalValue)}>Help @@ -104,13 +104,20 @@ export const DefineIndexOptions = ({ > { + setAccelerationFormData( + producer((accData) => { + accData.formErrors.indexNameError = validateIndexName(e.target.value); + }) + ); + }} /> {modalComponent} diff --git a/public/components/acceleration/selectors/index_setting_options.tsx b/public/components/acceleration/selectors/index_setting_options.tsx index 02ec6a2a..f5a0c69a 100644 --- a/public/components/acceleration/selectors/index_setting_options.tsx +++ b/public/components/acceleration/selectors/index_setting_options.tsx @@ -12,9 +12,17 @@ import { EuiSpacer, EuiText, } from '@elastic/eui'; +import producer from 'immer'; import React, { ChangeEvent, useState } from 'react'; import { ACCELERATION_TIME_INTERVAL } from '../../../../common/constants'; import { CreateAccelerationForm } from '../../../../common/types'; +import { + hasError, + validateCheckpointLocation, + validatePrimaryShardCount, + validateRefreshInterval, + validateReplicaCount, +} from '../create/utils'; import { IndexTypeSelector } from './index_type_selector'; interface IndexSettingOptionsProps { @@ -47,13 +55,13 @@ export const IndexSettingOptions = ({ const [checkpoint, setCheckpoint] = useState(''); const onChangePrimaryShards = (e: ChangeEvent) => { - const countPrimaryShards = +e.target.value; + const countPrimaryShards = parseInt(e.target.value, 10); setAccelerationFormData({ ...accelerationFormData, primaryShardsCount: countPrimaryShards }); setPrimaryShards(countPrimaryShards); }; const onChangeReplicaCount = (e: ChangeEvent) => { - const replicaCount = +e.target.value; + const replicaCount = parseInt(e.target.value, 10); setAccelerationFormData({ ...accelerationFormData, replicaShardsCount: replicaCount }); setReplicaCount(replicaCount); }; @@ -67,26 +75,22 @@ export const IndexSettingOptions = ({ }; const onChangeRefreshWindow = (e: ChangeEvent) => { - const windowCount = +e.target.value; - setAccelerationFormData({ - ...accelerationFormData, - refreshIntervalOptions: { - ...accelerationFormData.refreshIntervalOptions, - refreshWindow: windowCount, - }, - }); + const windowCount = parseInt(e.target.value, 10); + setAccelerationFormData( + producer((accData) => { + accData.refreshIntervalOptions.refreshWindow = windowCount; + }) + ); setRefreshWindow(windowCount); }; const onChangeRefreshInterval = (e: React.ChangeEvent) => { const refreshIntervalValue = e.target.value; - setAccelerationFormData({ - ...accelerationFormData, - refreshIntervalOptions: { - ...accelerationFormData.refreshIntervalOptions, - refreshInterval: refreshIntervalValue, - }, - }); + setAccelerationFormData( + producer((accData) => { + accData.refreshIntervalOptions.refreshInterval = refreshIntervalValue; + }) + ); setRefreshInterval(refreshIntervalValue); }; @@ -108,8 +112,9 @@ export const IndexSettingOptions = ({ /> { + setAccelerationFormData( + producer((accData) => { + accData.formErrors.primaryShardsError = validatePrimaryShardCount( + parseInt(e.target.value, 10) + ); + }) + ); + }} + isInvalid={hasError(accelerationFormData.formErrors, 'primaryShardsError')} /> { + setAccelerationFormData( + producer((accData) => { + accData.formErrors.replicaShardsError = validateReplicaCount( + parseInt(e.target.value, 10) + ); + }) + ); + }} + isInvalid={hasError(accelerationFormData.formErrors, 'replicaShardsError')} /> { + setAccelerationFormData( + producer((accData) => { + accData.formErrors.refreshIntervalError = validateRefreshInterval( + refreshTypeSelected, + parseInt(e.target.value, 10) + ); + }) + ); + }} append={ { + setAccelerationFormData( + producer((accData) => { + accData.formErrors.checkpointLocationError = validateCheckpointLocation( + accData.accelerationIndexType, + e.target.value + ); + }) + ); + }} /> diff --git a/public/components/acceleration/selectors/index_type_selector.tsx b/public/components/acceleration/selectors/index_type_selector.tsx index fe822322..06b7828d 100644 --- a/public/components/acceleration/selectors/index_type_selector.tsx +++ b/public/components/acceleration/selectors/index_type_selector.tsx @@ -6,6 +6,7 @@ import { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow, EuiLink, EuiText } from '@elastic/eui'; import React, { useState } from 'react'; import { + ACCELERATION_DEFUALT_SKIPPING_INDEX_NAME, ACCELERATION_INDEX_TYPES, ACC_INDEX_TYPE_DOCUMENTATION_URL, } from '../../../../common/constants'; @@ -23,6 +24,17 @@ export const IndexTypeSelector = ({ const [selectedIndexType, setSelectedIndexType] = useState[]>([ ACCELERATION_INDEX_TYPES[0], ]); + + const onChangeIndexType = (indexTypeOption: EuiComboBoxOptionOption[]) => { + const indexType = indexTypeOption[0].value as AccelerationIndexType; + setAccelerationFormData({ + ...accelerationFormData, + accelerationIndexType: indexType, + accelerationIndexName: + indexType === 'skipping' ? ACCELERATION_DEFUALT_SKIPPING_INDEX_NAME : '', + }); + setSelectedIndexType(indexTypeOption); + }; return ( <> { - setAccelerationFormData({ - ...accelerationFormData, - accelerationIndexType: indexType[0].value as AccelerationIndexType, - }); - setSelectedIndexType(indexType); - }} + onChange={onChangeIndexType} isInvalid={selectedIndexType.length === 0} isClearable={false} /> diff --git a/public/components/acceleration/selectors/source_selector.tsx b/public/components/acceleration/selectors/source_selector.tsx index 02ffc251..a0f068ba 100644 --- a/public/components/acceleration/selectors/source_selector.tsx +++ b/public/components/acceleration/selectors/source_selector.tsx @@ -11,8 +11,10 @@ import { EuiText, htmlIdGenerator, } from '@elastic/eui'; +import producer from 'immer'; import React, { useEffect, useState } from 'react'; import { CreateAccelerationForm } from '../../../../common/types'; +import { hasError, validateDataSource } from '../create/utils'; interface AccelerationDataSourceSelectorProps { accelerationFormData: CreateAccelerationForm; @@ -109,6 +111,8 @@ export const AccelerationDataSourceSelector = ({ { - setAccelerationFormData({ - ...accelerationFormData, - dataSource: dataConnectionOptions[0].label, - }); + setAccelerationFormData( + producer((accData) => { + accData.dataSource = dataConnectionOptions[0].label; + accData.formErrors.dataSourceError = validateDataSource( + dataConnectionOptions[0].label + ); + }) + ); setSelectedDataConnection(dataConnectionOptions); }} - isInvalid={selectedDataConnection.length === 0} isClearable={false} + isInvalid={hasError(accelerationFormData.formErrors, 'dataSourceError')} /> { - setAccelerationFormData({ - ...accelerationFormData, - database: tableOptions[0].label, - }); - setSelectedDatabase(tableOptions); + onChange={(databaseOptions) => { + setAccelerationFormData( + producer((accData) => { + accData.database = databaseOptions[0].label; + accData.formErrors.databaseError = validateDataSource(databaseOptions[0].label); + }) + ); + setSelectedDatabase(databaseOptions); }} - isInvalid={selectedDatabase.length === 0} isClearable={false} + isInvalid={hasError(accelerationFormData.formErrors, 'databaseError')} /> { - setAccelerationFormData({ - ...accelerationFormData, - dataTable: tableOptions[0].label, - }); + setAccelerationFormData( + producer((accData) => { + accData.dataTable = tableOptions[0].label; + accData.formErrors.dataTableError = validateDataSource(tableOptions[0].label); + }) + ); setSelectedTable(tableOptions); }} - isInvalid={selectedTable.length === 0} isClearable={false} + isInvalid={hasError(accelerationFormData.formErrors, 'dataTableError')} /> diff --git a/public/components/acceleration/visual_editors/__tests__/__snapshots__/query_visual_editor.test.tsx.snap b/public/components/acceleration/visual_editors/__tests__/__snapshots__/query_visual_editor.test.tsx.snap new file mode 100644 index 00000000..8659a435 --- /dev/null +++ b/public/components/acceleration/visual_editors/__tests__/__snapshots__/query_visual_editor.test.tsx.snap @@ -0,0 +1,1126 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Visual builder components renders visual builder with covering index options 1`] = ` +Array [ +
, + Array [ +
+

+ Covering index definition +

+
, +
, +
+
+ + + CREATE INDEX + + + + cv-idx + + +
+
+ + + [IF NOT EXISTS] + + + +
+
+ + + ON + + + + .. + + +
+
+ +
+
+
+
, + ], +] +`; + +exports[`Visual builder components renders visual builder with default options 1`] = ` +Array [ +
, + Array [ +
+

+ Skipping index definition +

+
, +
, +
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + +
+
+ + + Field name + + + + + + Datatype + + + + + + Acceleration method + + + + + + Delete + + +
+
+ + No items found + +
+
+
+
, +
+
+ +
+
+ +
+
, + ], +] +`; + +exports[`Visual builder components renders visual builder with materialized view options 1`] = ` +Array [ +
, + Array [ +
+

+ Materialized view definition +

+
, +
, + + + CREATE MATERIALIZED VIEW + + + + ..skipping + + , +
+ + + [IF NOT EXISTS] + + + +
, +
+
+ + + AS SELECT + + + +
+
+
+
+ +
+
+
+
, +
, +
, + + + FROM + + + + .. + + , +
, +
+
+
+ +
+
+
, + ], +] +`; + +exports[`Visual builder components renders visual builder with skipping index options 1`] = ` +Array [ +
, + Array [ +
+

+ Skipping index definition +

+
, +
, +
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + Field name + + + + + + Datatype + + + + + + Acceleration method + + + + + + Delete + + +
+
+ Field name +
+
+ + field1 + +
+
+
+ Datatype +
+
+ + string + +
+
+
+ Acceleration method +
+
+
+
+ +
+ + + +
+
+
+
+
+
+ Delete +
+
+ +
+
+
+ Field name +
+
+ + field2 + +
+
+
+ Datatype +
+
+ + number + +
+
+
+ Acceleration method +
+
+
+
+ +
+ + + +
+
+
+
+
+
+ Delete +
+
+ +
+
+
+
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
, +
+
+ +
+
+ +
+
, + ], +] +`; diff --git a/public/components/acceleration/visual_editors/__tests__/query_builder.test.tsx b/public/components/acceleration/visual_editors/__tests__/query_builder.test.tsx new file mode 100644 index 00000000..2aca0b68 --- /dev/null +++ b/public/components/acceleration/visual_editors/__tests__/query_builder.test.tsx @@ -0,0 +1,84 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + coveringIndexBuilderMock1, + coveringIndexBuilderMock2, + coveringIndexBuilderMockResult1, + coveringIndexBuilderMockResult2, + indexOptionsMock1, + indexOptionsMock2, + indexOptionsMock3, + indexOptionsMockResult1, + indexOptionsMockResult2, + indexOptionsMockResult3, + materializedViewBuilderMock1, + materializedViewBuilderMock2, + materializedViewBuilderMockResult1, + materializedViewBuilderMockResult2, + skippingIndexBuilderMock1, + skippingIndexBuilderMock2, + skippingIndexBuilderMockResult1, + skippingIndexBuilderMockResult2, +} from '../../../../../test/mocks/accelerationMock'; +import { + buildIndexOptions, + coveringIndexQueryBuilder, + materializedQueryViewBuilder, + skippingIndexQueryBuilder, +} from '../query_builder'; + +describe('buildIndexOptions', () => { + it('should build index options with auto refresh', () => { + const indexOptions = buildIndexOptions(indexOptionsMock1); + expect(indexOptions).toEqual(indexOptionsMockResult1); + }); + + it('should build index options with interval refresh', () => { + const indexOptions = buildIndexOptions(indexOptionsMock2); + expect(indexOptions).toEqual(indexOptionsMockResult2); + }); + + it('should build index options with checkpoint location', () => { + const indexOptions = buildIndexOptions(indexOptionsMock3); + expect(indexOptions).toEqual(indexOptionsMockResult3); + }); + + describe('skippingIndexQueryBuilder', () => { + it('should build skipping index query as expected with interval refresh', () => { + const result = skippingIndexQueryBuilder(skippingIndexBuilderMock1); + expect(result).toEqual(skippingIndexBuilderMockResult1); + }); + + it('should build skipping index query as expected with auto refresh', () => { + const result = skippingIndexQueryBuilder(skippingIndexBuilderMock2); + expect(result).toEqual(skippingIndexBuilderMockResult2); + }); + }); + + describe('coveringIndexQueryBuilder', () => { + it('should build covering index query as expected with interval refresh', () => { + const result = coveringIndexQueryBuilder(coveringIndexBuilderMock1); + expect(result).toEqual(coveringIndexBuilderMockResult1); + }); + + it('should build covering index query as expected with auto refresh', () => { + const result = coveringIndexQueryBuilder(coveringIndexBuilderMock2); + expect(result).toEqual(coveringIndexBuilderMockResult2); + }); + }); + + describe('materializedQueryViewBuilder', () => { + it('should build materialized view query as expected with interval refresh', () => { + const result = materializedQueryViewBuilder(materializedViewBuilderMock1); + expect(result).toEqual(materializedViewBuilderMockResult1); + }); + + it('should build materialized view query as expected with auto refresh', () => { + const result = materializedQueryViewBuilder(materializedViewBuilderMock2); + expect(result).toEqual(materializedViewBuilderMockResult2); + }); + }); +}); diff --git a/public/components/acceleration/visual_editors/__tests__/query_visual_editor.test.tsx b/public/components/acceleration/visual_editors/__tests__/query_visual_editor.test.tsx new file mode 100644 index 00000000..38513b86 --- /dev/null +++ b/public/components/acceleration/visual_editors/__tests__/query_visual_editor.test.tsx @@ -0,0 +1,116 @@ +/* + * 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 toJson from 'enzyme-to-json'; +import React from 'react'; +import { CreateAccelerationForm } from '../../../../../common/types'; +import { + coveringIndexDataMock, + createAccelerationEmptyDataMock, + materializedViewValidDataMock, + skippingIndexDataMock, +} from '../../../../../test/mocks/accelerationMock'; +import { QueryVisualEditor } from '../query_visual_editor'; + +describe('Visual builder components', () => { + configure({ adapter: new Adapter() }); + + it('renders visual builder with default options', async () => { + const accelerationFormData = createAccelerationEmptyDataMock; + const setAccelerationFormData = jest.fn(); + const wrapper = mount( + + ); + wrapper.update(); + await waitFor(() => { + expect( + toJson(wrapper, { + noKey: false, + mode: 'deep', + }) + ).toMatchSnapshot(); + }); + }); + + it('renders visual builder with skipping index options', async () => { + const accelerationFormData: CreateAccelerationForm = { + ...createAccelerationEmptyDataMock, + accelerationIndexName: 'skipping', + accelerationIndexType: 'skipping', + skippingIndexQueryData: skippingIndexDataMock, + }; + const setAccelerationFormData = jest.fn(); + const wrapper = mount( + + ); + wrapper.update(); + await waitFor(() => { + expect( + toJson(wrapper, { + noKey: false, + mode: 'deep', + }) + ).toMatchSnapshot(); + }); + }); + + it('renders visual builder with covering index options', async () => { + const accelerationFormData: CreateAccelerationForm = { + ...createAccelerationEmptyDataMock, + accelerationIndexName: 'cv-idx', + accelerationIndexType: 'covering', + coveringIndexQueryData: coveringIndexDataMock, + }; + const setAccelerationFormData = jest.fn(); + const wrapper = mount( + + ); + wrapper.update(); + await waitFor(() => { + expect( + toJson(wrapper, { + noKey: false, + mode: 'deep', + }) + ).toMatchSnapshot(); + }); + }); + + it('renders visual builder with materialized view options', async () => { + const accelerationFormData: CreateAccelerationForm = { + ...createAccelerationEmptyDataMock, + accelerationIndexType: 'materialized', + materializedViewQueryData: materializedViewValidDataMock, + }; + const setAccelerationFormData = jest.fn(); + const wrapper = mount( + + ); + wrapper.update(); + await waitFor(() => { + expect( + toJson(wrapper, { + noKey: false, + mode: 'deep', + }) + ).toMatchSnapshot(); + }); + }); +}); diff --git a/public/components/acceleration/visual_editors/covering_index/__tests__/__snapshots__/covering_index_builder.test.tsx.snap b/public/components/acceleration/visual_editors/covering_index/__tests__/__snapshots__/covering_index_builder.test.tsx.snap new file mode 100644 index 00000000..3d237119 --- /dev/null +++ b/public/components/acceleration/visual_editors/covering_index/__tests__/__snapshots__/covering_index_builder.test.tsx.snap @@ -0,0 +1,191 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Covering index builder components renders covering index builder with different options 1`] = ` +Array [ +
+

+ Covering index definition +

+
, +
, +
+
+ + + CREATE INDEX + + + + skipping + + +
+
+ + + [IF NOT EXISTS] + + + +
+
+ + + ON + + + + .. + + +
+
+ +
+
+
+
, +] +`; + +exports[`Covering index builder components renders covering index builder with default options 1`] = ` +Array [ +
+

+ Covering index definition +

+
, +
, +
+
+ + + CREATE INDEX + + + + skipping + + +
+
+ + + [IF NOT EXISTS] + + + +
+
+ + + ON + + + + .. + + +
+
+ +
+
+
+
, +] +`; diff --git a/public/components/acceleration/visual_editors/covering_index/__tests__/covering_index_builder.test.tsx b/public/components/acceleration/visual_editors/covering_index/__tests__/covering_index_builder.test.tsx new file mode 100644 index 00000000..18a73bae --- /dev/null +++ b/public/components/acceleration/visual_editors/covering_index/__tests__/covering_index_builder.test.tsx @@ -0,0 +1,60 @@ +/* + * 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 toJson from 'enzyme-to-json'; +import React from 'react'; +import { CreateAccelerationForm } from '../../../../../../common/types'; +import { createAccelerationEmptyDataMock } from '../../../../../../test/mocks/accelerationMock'; +import { CoveringIndexBuilder } from '../covering_index_builder'; + +describe('Covering index builder components', () => { + configure({ adapter: new Adapter() }); + + it('renders covering index builder with default options', async () => { + const accelerationFormData = createAccelerationEmptyDataMock; + const setAccelerationFormData = jest.fn(); + const wrapper = mount( + + ); + wrapper.update(); + await waitFor(() => { + expect( + toJson(wrapper, { + noKey: false, + mode: 'deep', + }) + ).toMatchSnapshot(); + }); + }); + + it('renders covering index builder with different options', async () => { + const accelerationFormData: CreateAccelerationForm = { + ...createAccelerationEmptyDataMock, + coveringIndexQueryData: ['field1', 'field2', 'field3'], + }; + const setAccelerationFormData = jest.fn(); + const wrapper = mount( + + ); + wrapper.update(); + await waitFor(() => { + expect( + toJson(wrapper, { + noKey: false, + mode: 'deep', + }) + ).toMatchSnapshot(); + }); + }); +}); 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/__tests__/__snapshots__/add_column_popover.test.tsx.snap b/public/components/acceleration/visual_editors/materialized_view/__tests__/__snapshots__/add_column_popover.test.tsx.snap new file mode 100644 index 00000000..ffb0224d --- /dev/null +++ b/public/components/acceleration/visual_editors/materialized_view/__tests__/__snapshots__/add_column_popover.test.tsx.snap @@ -0,0 +1,84 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Column popover components in materialized view renders column popover components in materialized view with default options 1`] = ` +
+
+ +
+
+`; + +exports[`Column popover components in materialized view renders column popover components in materialized view with different options 1`] = ` +
+
+ +
+
+`; diff --git a/public/components/acceleration/visual_editors/materialized_view/__tests__/__snapshots__/column_expression.test.tsx.snap b/public/components/acceleration/visual_editors/materialized_view/__tests__/__snapshots__/column_expression.test.tsx.snap new file mode 100644 index 00000000..61725510 --- /dev/null +++ b/public/components/acceleration/visual_editors/materialized_view/__tests__/__snapshots__/column_expression.test.tsx.snap @@ -0,0 +1,175 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Column expression components in materialized view renders column expression components in materialized view with default options 1`] = ` +
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+`; + +exports[`Column expression components in materialized view renders column expression components in materialized view with different options 1`] = ` +
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+`; diff --git a/public/components/acceleration/visual_editors/materialized_view/__tests__/__snapshots__/group_by_tumble_expression.test.tsx.snap b/public/components/acceleration/visual_editors/materialized_view/__tests__/__snapshots__/group_by_tumble_expression.test.tsx.snap new file mode 100644 index 00000000..235e817d --- /dev/null +++ b/public/components/acceleration/visual_editors/materialized_view/__tests__/__snapshots__/group_by_tumble_expression.test.tsx.snap @@ -0,0 +1,65 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Group by components in materialized view renders group by components in materialized view with default options 1`] = ` +
+
+
+ +
+
+
+`; + +exports[`Group by components in materialized view renders group by components in materialized view with different options 1`] = ` +
+
+
+ +
+
+
+`; diff --git a/public/components/acceleration/visual_editors/materialized_view/__tests__/__snapshots__/materialized_view_builder.test.tsx.snap b/public/components/acceleration/visual_editors/materialized_view/__tests__/__snapshots__/materialized_view_builder.test.tsx.snap new file mode 100644 index 00000000..75955730 --- /dev/null +++ b/public/components/acceleration/visual_editors/materialized_view/__tests__/__snapshots__/materialized_view_builder.test.tsx.snap @@ -0,0 +1,318 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Builder components in materialized view renders builder components in materialized view with default options 1`] = ` +Array [ +
+

+ Materialized view definition +

+
, +
, + + + CREATE MATERIALIZED VIEW + + + + ..skipping + + , +
+ + + [IF NOT EXISTS] + + + +
, +
+
+ + + AS SELECT + + + +
+
+
+
+ +
+
+
+
, +
, +
, + + + FROM + + + + .. + + , +
, +
+
+
+ +
+
+
, +] +`; + +exports[`Builder components in materialized view renders builder components in materialized view with different options 1`] = ` +Array [ +
+

+ Materialized view definition +

+
, +
, + + + CREATE MATERIALIZED VIEW + + + + ..skipping + + , +
+ + + [IF NOT EXISTS] + + + +
, +
+
+ + + AS SELECT + + + +
+
+
+
+ +
+
+
+
, +
, +
, + + + FROM + + + + .. + + , +
, +
+
+
+ +
+
+
, +] +`; diff --git a/public/components/acceleration/visual_editors/materialized_view/__tests__/add_column_popover.test.tsx b/public/components/acceleration/visual_editors/materialized_view/__tests__/add_column_popover.test.tsx new file mode 100644 index 00000000..56c40581 --- /dev/null +++ b/public/components/acceleration/visual_editors/materialized_view/__tests__/add_column_popover.test.tsx @@ -0,0 +1,76 @@ +/* + * 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 toJson from 'enzyme-to-json'; +import React from 'react'; +import { CreateAccelerationForm } from '../../../../../../common/types'; +import { + createAccelerationEmptyDataMock, + materializedViewValidDataMock, +} from '../../../../../../test/mocks/accelerationMock'; +import { AddColumnPopOver } from '../add_column_popover'; + +describe('Column popover components in materialized view', () => { + configure({ adapter: new Adapter() }); + + it('renders column popover components in materialized view with default options', async () => { + const accelerationFormData = createAccelerationEmptyDataMock; + const setAccelerationFormData = jest.fn(); + const setIsColumnPopOverOpen = jest.fn(); + const setColumnExpressionValues = jest.fn(); + const wrapper = mount( + + ); + wrapper.update(); + await waitFor(() => { + expect( + toJson(wrapper, { + noKey: false, + mode: 'deep', + }) + ).toMatchSnapshot(); + }); + }); + + it('renders column popover components in materialized view with different options', async () => { + const accelerationFormData: CreateAccelerationForm = { + ...createAccelerationEmptyDataMock, + accelerationIndexType: 'materialized', + materializedViewQueryData: materializedViewValidDataMock, + }; + const setAccelerationFormData = jest.fn(); + const setIsColumnPopOverOpen = jest.fn(); + const setColumnExpressionValues = jest.fn(); + const wrapper = mount( + + ); + wrapper.update(); + await waitFor(() => { + expect( + toJson(wrapper, { + noKey: false, + mode: 'deep', + }) + ).toMatchSnapshot(); + }); + }); +}); diff --git a/public/components/acceleration/visual_editors/materialized_view/__tests__/column_expression.test.tsx b/public/components/acceleration/visual_editors/materialized_view/__tests__/column_expression.test.tsx new file mode 100644 index 00000000..4ce888f5 --- /dev/null +++ b/public/components/acceleration/visual_editors/materialized_view/__tests__/column_expression.test.tsx @@ -0,0 +1,78 @@ +/* + * 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 toJson from 'enzyme-to-json'; +import React from 'react'; +import { CreateAccelerationForm } from '../../../../../../common/types'; +import { + createAccelerationEmptyDataMock, + materializedViewValidDataMock, +} from '../../../../../../test/mocks/accelerationMock'; +import { ColumnExpression } from '../column_expression'; + +describe('Column expression components in materialized view', () => { + configure({ adapter: new Adapter() }); + + it('renders column expression components in materialized view with default options', async () => { + const accelerationFormData: CreateAccelerationForm = { + ...createAccelerationEmptyDataMock, + accelerationIndexType: 'materialized', + materializedViewQueryData: materializedViewValidDataMock, + }; + const setAccelerationFormData = jest.fn(); + const setColumnExpressionValues = jest.fn(); + const wrapper = mount( + + ); + wrapper.update(); + await waitFor(() => { + expect( + toJson(wrapper, { + noKey: false, + mode: 'deep', + }) + ).toMatchSnapshot(); + }); + }); + + it('renders column expression components in materialized view with different options', async () => { + const accelerationFormData: CreateAccelerationForm = { + ...createAccelerationEmptyDataMock, + accelerationIndexType: 'materialized', + materializedViewQueryData: materializedViewValidDataMock, + }; + const setAccelerationFormData = jest.fn(); + const setColumnExpressionValues = jest.fn(); + const wrapper = mount( + + ); + wrapper.update(); + await waitFor(() => { + expect( + toJson(wrapper, { + noKey: false, + mode: 'deep', + }) + ).toMatchSnapshot(); + }); + }); +}); diff --git a/public/components/acceleration/visual_editors/materialized_view/__tests__/group_by_tumble_expression.test.tsx b/public/components/acceleration/visual_editors/materialized_view/__tests__/group_by_tumble_expression.test.tsx new file mode 100644 index 00000000..1a94eb3f --- /dev/null +++ b/public/components/acceleration/visual_editors/materialized_view/__tests__/group_by_tumble_expression.test.tsx @@ -0,0 +1,64 @@ +/* + * 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 toJson from 'enzyme-to-json'; +import React from 'react'; +import { CreateAccelerationForm } from '../../../../../../common/types'; +import { + createAccelerationEmptyDataMock, + materializedViewValidDataMock, +} from '../../../../../../test/mocks/accelerationMock'; +import { GroupByTumbleExpression } from '../group_by_tumble_expression'; + +describe('Group by components in materialized view', () => { + configure({ adapter: new Adapter() }); + + it('renders group by components in materialized view with default options', async () => { + const accelerationFormData = createAccelerationEmptyDataMock; + const setAccelerationFormData = jest.fn(); + const wrapper = mount( + + ); + wrapper.update(); + await waitFor(() => { + expect( + toJson(wrapper, { + noKey: false, + mode: 'deep', + }) + ).toMatchSnapshot(); + }); + }); + + it('renders group by components in materialized view with different options', async () => { + const accelerationFormData: CreateAccelerationForm = { + ...createAccelerationEmptyDataMock, + accelerationIndexType: 'materialized', + materializedViewQueryData: materializedViewValidDataMock, + }; + const setAccelerationFormData = jest.fn(); + const wrapper = mount( + + ); + wrapper.update(); + await waitFor(() => { + expect( + toJson(wrapper, { + noKey: false, + mode: 'deep', + }) + ).toMatchSnapshot(); + }); + }); +}); diff --git a/public/components/acceleration/visual_editors/materialized_view/__tests__/materialized_view_builder.test.tsx b/public/components/acceleration/visual_editors/materialized_view/__tests__/materialized_view_builder.test.tsx new file mode 100644 index 00000000..2075c6b0 --- /dev/null +++ b/public/components/acceleration/visual_editors/materialized_view/__tests__/materialized_view_builder.test.tsx @@ -0,0 +1,64 @@ +/* + * 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 toJson from 'enzyme-to-json'; +import React from 'react'; +import { CreateAccelerationForm } from '../../../../../../common/types'; +import { + createAccelerationEmptyDataMock, + materializedViewValidDataMock, +} from '../../../../../../test/mocks/accelerationMock'; +import { MaterializedViewBuilder } from '../materialized_view_builder'; + +describe('Builder components in materialized view', () => { + configure({ adapter: new Adapter() }); + + it('renders builder components in materialized view with default options', async () => { + const accelerationFormData = createAccelerationEmptyDataMock; + const setAccelerationFormData = jest.fn(); + const wrapper = mount( + + ); + wrapper.update(); + await waitFor(() => { + expect( + toJson(wrapper, { + noKey: false, + mode: 'deep', + }) + ).toMatchSnapshot(); + }); + }); + + it('renders builder components in materialized view with different options', async () => { + const accelerationFormData: CreateAccelerationForm = { + ...createAccelerationEmptyDataMock, + accelerationIndexType: 'materialized', + materializedViewQueryData: materializedViewValidDataMock, + }; + const setAccelerationFormData = jest.fn(); + const wrapper = mount( + + ); + wrapper.update(); + await waitFor(() => { + expect( + toJson(wrapper, { + noKey: false, + mode: 'deep', + }) + ).toMatchSnapshot(); + }); + }); +}); 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 f61bdf99..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 @@ -17,6 +17,8 @@ import { EuiSpacer, 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 { @@ -24,6 +26,7 @@ import { CreateAccelerationForm, MaterializedViewColumn, } from '../../../../../common/types'; +import { validateMaterializedViewData } from '../../create/utils'; interface AddColumnPopOverProps { isColumnPopOverOpen: boolean; @@ -31,6 +34,7 @@ interface AddColumnPopOverProps { columnExpressionValues: MaterializedViewColumn[]; setColumnExpressionValues: React.Dispatch>; accelerationFormData: CreateAccelerationForm; + setAccelerationFormData: React.Dispatch>; } export const AddColumnPopOver = ({ @@ -39,11 +43,12 @@ export const AddColumnPopOver = ({ columnExpressionValues, setColumnExpressionValues, accelerationFormData, + setAccelerationFormData, }: AddColumnPopOverProps) => { const [selectedFunction, setSelectedFunction] = useState([ ACCELERATION_AGGREGRATION_FUNCTIONS[0], ]); - const [selectedField, setSelectedField] = useState([]); + const [selectedField, setSelectedField] = useState([]); const [selectedAlias, setSeletedAlias] = useState(''); const resetSelectedField = () => { @@ -63,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(); }, []); @@ -96,6 +126,7 @@ export const AddColumnPopOver = ({ options={ACCELERATION_AGGREGRATION_FUNCTIONS} selectedOptions={selectedFunction} onChange={setSelectedFunction} + isClearable={false} /> @@ -109,6 +140,7 @@ export const AddColumnPopOver = ({ ]} selectedOptions={selectedField} onChange={setSelectedField} + isClearable={false} /> @@ -119,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..ba56488f 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: parseInt(e.target.value, 10) }; + 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/components/acceleration/visual_editors/query_builder.tsx b/public/components/acceleration/visual_editors/query_builder.tsx index 5be5e6df..49d4482f 100644 --- a/public/components/acceleration/visual_editors/query_builder.tsx +++ b/public/components/acceleration/visual_editors/query_builder.tsx @@ -12,7 +12,7 @@ import { import { pluralizeTime } from '../create/utils'; /* Add index options to query */ -const buildIndexOptions = (accelerationformData: CreateAccelerationForm) => { +export const buildIndexOptions = (accelerationformData: CreateAccelerationForm) => { const { primaryShardsCount, replicaShardsCount, @@ -71,7 +71,7 @@ const buildSkippingIndexColumns = (skippingIndexQueryData: SkippingIndexRowType[ * index_settings = '{"number_of_shards":9,"number_of_replicas":2}' * ) */ -const skippingIndexQueryBuilder = (accelerationformData: CreateAccelerationForm) => { +export const skippingIndexQueryBuilder = (accelerationformData: CreateAccelerationForm) => { const { dataSource, database, dataTable, skippingIndexQueryData } = accelerationformData; const codeQuery = `CREATE SKIPPING INDEX @@ -107,7 +107,7 @@ const buildCoveringIndexColumns = (coveringIndexQueryData: string[]) => { * index_settings = '{"number_of_shards":9,"number_of_replicas":2}' * ) */ -const coveringIndexQueryBuilder = (accelerationformData: CreateAccelerationForm) => { +export const coveringIndexQueryBuilder = (accelerationformData: CreateAccelerationForm) => { const { dataSource, database, @@ -130,8 +130,8 @@ const buildMaterializedViewColumns = (columnsValues: MaterializedViewColumn[]) = return columnsValues .map( (column) => - ` ${column.functionName}(${column.functionParam}) ${ - column.fieldAlias && `AS ${column.fieldAlias}` + ` ${column.functionName}(${column.functionParam})${ + column.fieldAlias ? ` AS ${column.fieldAlias}` : `` }` ) .join(', \n'); @@ -161,7 +161,7 @@ const buildTumbleValue = (GroupByTumbleValue: GroupByTumbleType) => { * index_settings = '{"number_of_shards":9,"number_of_replicas":2}' * ) */ -const materializedQueryViewBuilder = (accelerationformData: CreateAccelerationForm) => { +export const materializedQueryViewBuilder = (accelerationformData: CreateAccelerationForm) => { const { dataSource, database, diff --git a/public/components/acceleration/visual_editors/skipping_index/__tests__/__snapshots__/add_fields_modal.test.tsx.snap b/public/components/acceleration/visual_editors/skipping_index/__tests__/__snapshots__/add_fields_modal.test.tsx.snap new file mode 100644 index 00000000..f2f42748 --- /dev/null +++ b/public/components/acceleration/visual_editors/skipping_index/__tests__/__snapshots__/add_fields_modal.test.tsx.snap @@ -0,0 +1,1509 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Add fields modal in skipping index renders add fields modal in skipping index with default options 1`] = ` + +