From 15ae2d3cc3872c6db94529472654236baf64dd1f Mon Sep 17 00:00:00 2001 From: Ashish Gupta Date: Tue, 9 Jul 2024 14:57:12 +0530 Subject: [PATCH] MINOR: supported all_index in search index configuration form (#16571) * supported all_index in search index configuration form * allow clear in select widget * supported tree select for the entities * playwright test * added env for the run application status test * fix beta badge color, color of checkbox and changes as per comments * minor fix * fix sonar issue --- .../data/app/SearchIndexingApplication.json | 5 +- .../SearchIndexingApplication.json | 8 +- .../e2e/Pages/SearchIndexApplication.spec.ts | 217 ++++++++++++++++++ .../ui/playwright/utils/customProperty.ts | 2 +- .../AddService/Steps/SelectServiceType.tsx | 8 +- .../JsonSchemaWidgets/SelectWidget.test.tsx | 42 +++- .../JsonSchemaWidgets/SelectWidget.tsx | 15 +- .../TreeSelectWidget.test.tsx | 100 ++++++++ .../JsonSchemaWidgets/TreeSelectWidget.tsx | 72 ++++++ .../ui/src/mocks/SelectWidget.mock.ts | 169 ++++++++++++++ .../ui/src/styles/components/rjsf.less | 2 + .../SearchIndexingApplication.json | 40 +--- 12 files changed, 616 insertions(+), 64 deletions(-) create mode 100644 openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/SearchIndexApplication.spec.ts create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/common/Form/JSONSchema/JsonSchemaWidgets/TreeSelectWidget.test.tsx create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/common/Form/JSONSchema/JsonSchemaWidgets/TreeSelectWidget.tsx diff --git a/openmetadata-service/src/main/resources/json/data/app/SearchIndexingApplication.json b/openmetadata-service/src/main/resources/json/data/app/SearchIndexingApplication.json index 5ca7d2ec8217..a9939936e444 100644 --- a/openmetadata-service/src/main/resources/json/data/app/SearchIndexingApplication.json +++ b/openmetadata-service/src/main/resources/json/data/app/SearchIndexingApplication.json @@ -29,13 +29,14 @@ "dashboardService", "pipelineService", "mlmodelService", + "storageService", + "metadataService", "searchService", "entityReportData", "webAnalyticEntityViewReportData", "webAnalyticUserActivityReportData", "domain", "storedProcedure", - "storageService", "dataProduct", "testCaseResolutionStatus" ], @@ -47,4 +48,4 @@ "scheduleTimeline": "Custom", "cronExpression": "0 0 * * *" } -} \ No newline at end of file +} diff --git a/openmetadata-service/src/main/resources/json/data/appMarketPlaceDefinition/SearchIndexingApplication.json b/openmetadata-service/src/main/resources/json/data/appMarketPlaceDefinition/SearchIndexingApplication.json index a5371efbf407..90d6dbfc4c14 100644 --- a/openmetadata-service/src/main/resources/json/data/appMarketPlaceDefinition/SearchIndexingApplication.json +++ b/openmetadata-service/src/main/resources/json/data/appMarketPlaceDefinition/SearchIndexingApplication.json @@ -43,17 +43,19 @@ "dashboardService", "pipelineService", "mlmodelService", + "storageService", + "metadataService", "searchService", "entityReportData", "webAnalyticEntityViewReportData", "webAnalyticUserActivityReportData", "domain", "storedProcedure", - "storageService", - "dataProduct" + "dataProduct", + "testCaseResolutionStatus" ], "recreateIndex": true, "batchSize": "100", "searchIndexMappingLanguage": "EN" } -} \ No newline at end of file +} diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/SearchIndexApplication.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/SearchIndexApplication.spec.ts new file mode 100644 index 000000000000..2674df203023 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/SearchIndexApplication.spec.ts @@ -0,0 +1,217 @@ +/* + * Copyright 2024 Collate. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import test, { expect, Page } from '@playwright/test'; +import { GlobalSettingOptions } from '../../constant/settings'; +import { getApiContext, redirectToHomePage } from '../../utils/common'; +import { settingClick } from '../../utils/sidebar'; + +// use the admin user to login +test.use({ storageState: 'playwright/.auth/admin.json' }); + +const verifyApplicationTriggerToastData = async (page: Page) => { + await expect(page.getByRole('alert').first()).toHaveText( + /Application triggered successfully/ + ); + + await page.getByLabel('close').first().click(); +}; + +const verifyLastExecutionStatus = async (page: Page) => { + const { apiContext } = await getApiContext(page); + + await expect + .poll( + async () => { + const response = await apiContext + .get( + '/api/v1/apps/name/SearchIndexingApplication/status?offset=0&limit=1' + ) + .then((res) => res.json()); + + return response.data[0]?.status; + }, + { + // Custom expect message for reporting, optional. + message: 'To get the last run execution status as success', + intervals: [30_000], + timeout: 300_000, + } + ) + .toBe('success'); + + await page.reload(); + + await page.waitForSelector('[data-testid="app-run-history-table"]'); + + await expect(page.getByTestId('pipeline-status')).toContainText('Success'); +}; + +const verifyLastExecutionRun = async (page: Page) => { + const response = await page.waitForResponse( + '/api/v1/apps/name/SearchIndexingApplication/status?offset=0&limit=1' + ); + + expect(response.status()).toBe(200); + + const responseData = await response.json(); + if (responseData.data.length > 0) { + expect(responseData.data).toHaveLength(1); + + if (responseData.data[0].status === 'running') { + // wait for success status + await verifyLastExecutionStatus(page); + } else { + expect(responseData.data[0].status).toBe('success'); + } + } +}; + +test('Search Index Application', async ({ page }) => { + await test.step('Visit Application page', async () => { + await redirectToHomePage(page); + await settingClick(page, GlobalSettingOptions.APPLICATIONS); + }); + + await test.step('Verify last execution run', async () => { + await page + .locator( + '[data-testid="search-indexing-application-card"] [data-testid="config-btn"]' + ) + .click(); + await verifyLastExecutionRun(page); + }); + + await test.step('Edit application', async () => { + await page.click('[data-testid="edit-button"]'); + await page.click('[data-testid="cron-type"]'); + await page.click('.rc-virtual-list [title="None"]'); + + const deployResponse = page.waitForResponse('/api/v1/apps/*'); + await page.click('.ant-modal-body [data-testid="deploy-button"]'); + await deployResponse; + + await expect(page.getByRole('alert').first()).toHaveText( + /Schedule saved successfully/ + ); + + await page.getByLabel('close').first().click(); + + expect(await page.innerText('[data-testid="schedule-type"]')).toContain( + 'None' + ); + + await page.click('[data-testid="configuration"]'); + await page.fill('#root\\/batchSize', '0'); + + await page.getByTitle('chart').getByLabel('close').click(); + + await page.click( + '[data-testid="select-widget"] > .ant-select-selector > .ant-select-selection-item' + ); + await page.click('[data-testid="select-option-JP"]'); + + const responseAfterSubmit = page.waitForResponse('/api/v1/apps/*'); + await page.click('[data-testid="submit-btn"]'); + await responseAfterSubmit; + + await expect(page.getByRole('alert').first()).toHaveText( + /Configuration saved successfully/ + ); + + await page.getByLabel('close').first().click(); + }); + + await test.step('Uninstall application', async () => { + await page.click('[data-testid="manage-button"]'); + await page.click('[data-testid="uninstall-button-title"]'); + + const deleteRequest = page.waitForResponse( + '/api/v1/apps/name/SearchIndexingApplication?hardDelete=true' + ); + await page.click('[data-testid="save-button"]'); + await deleteRequest; + + await expect(page.getByRole('alert').first()).toHaveText( + /Application uninstalled/ + ); + + await page.getByLabel('close').first().click(); + + const card1 = page.locator( + '[data-testid="search-indexing-application-card"]' + ); + + expect(await card1.isVisible()).toBe(false); + }); + + await test.step('Install application', async () => { + await page.click('[data-testid="add-application"]'); + + // Verify response status code + const getMarketPlaceResponse = await page.waitForResponse( + '/api/v1/apps/marketplace?limit=*' + ); + + expect(getMarketPlaceResponse.status()).toBe(200); + + await page.click( + '[data-testid="search-indexing-application-card"] [data-testid="config-btn"]' + ); + await page.click('[data-testid="install-application"]'); + await page.click('[data-testid="save-button"]'); + await page.click('[data-testid="submit-btn"]'); + await page.click('[data-testid="cron-type"]'); + await page.click('.rc-virtual-list [title="None"]'); + + expect(await page.innerText('[data-testid="cron-type"]')).toContain('None'); + + const installApplicationResponse = page.waitForResponse('api/v1/apps'); + await page.click('[data-testid="deploy-button"]'); + await installApplicationResponse; + + await expect(page.getByRole('alert').first()).toHaveText( + /Application installed successfully/ + ); + + await page.getByLabel('close').first().click(); + + const card = page.locator( + '[data-testid="search-indexing-application-card"]' + ); + + expect(await card.isVisible()).toBe(true); + }); + + if (process.env.isOss) { + await test.step('Run application', async () => { + test.slow(true); // Test time shouldn't exceed while re-fetching the history API. + + await page.click( + '[data-testid="search-indexing-application-card"] [data-testid="config-btn"]' + ); + + const triggerPipelineResponse = page.waitForResponse( + '/api/v1/apps/trigger/SearchIndexingApplication' + ); + await page.click('[data-testid="run-now-button"]'); + + await triggerPipelineResponse; + + await verifyApplicationTriggerToastData(page); + + await page.reload(); + + await verifyLastExecutionRun(page); + }); + } +}); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/customProperty.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/customProperty.ts index c6cf6b1d9a0c..834dc5ed78c6 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/customProperty.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/customProperty.ts @@ -12,8 +12,8 @@ */ import { APIRequestContext, expect, Page } from '@playwright/test'; import { - ENTITY_PATH, EntityTypeEndpoint, + ENTITY_PATH, } from '../support/entity/Entity.interface'; import { UserClass } from '../support/user/UserClass'; import { uuid } from './common'; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddService/Steps/SelectServiceType.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddService/Steps/SelectServiceType.tsx index e3722d7eeaed..2568a2510bb5 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddService/Steps/SelectServiceType.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddService/Steps/SelectServiceType.tsx @@ -28,7 +28,6 @@ import { DatabaseServiceType } from '../../../../../generated/entity/data/databa import { MetadataServiceType } from '../../../../../generated/entity/services/metadataService'; import { MlModelServiceType } from '../../../../../generated/entity/services/mlmodelService'; import { PipelineServiceType } from '../../../../../generated/entity/services/pipelineService'; -import { useApplicationStore } from '../../../../../hooks/useApplicationStore'; import { errorMsg, getServiceLogo } from '../../../../../utils/CommonUtils'; import ServiceUtilClassBase from '../../../../../utils/ServiceUtilClassBase'; import Searchbar from '../../../../common/SearchBarComponent/SearchBar.component'; @@ -44,7 +43,6 @@ const SelectServiceType = ({ onCancel, onNext, }: SelectServiceTypeProps) => { - const { theme } = useApplicationStore(); const { t } = useTranslation(); const [category, setCategory] = useState(''); const [connectorSearchTerm, setConnectorSearchTerm] = useState(''); @@ -146,11 +144,7 @@ const SelectServiceType = ({ {BETA_SERVICES.includes( type as DatabaseServiceType | PipelineServiceType ) ? ( - + ) : null}

diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/Form/JSONSchema/JsonSchemaWidgets/SelectWidget.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/Form/JSONSchema/JsonSchemaWidgets/SelectWidget.test.tsx index 75f3172b10f6..4467ef5fa39b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/Form/JSONSchema/JsonSchemaWidgets/SelectWidget.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/Form/JSONSchema/JsonSchemaWidgets/SelectWidget.test.tsx @@ -22,32 +22,50 @@ import { } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; -import { MOCK_SELECT_WIDGET } from '../../../../../mocks/SelectWidget.mock'; +import { + MOCK_SELECT_WIDGET, + MOCK_TREE_SELECT_WIDGET, +} from '../../../../../mocks/SelectWidget.mock'; import SelectWidget from './SelectWidget'; +jest.mock('./TreeSelectWidget', () => + jest.fn().mockImplementation(() =>

TreeSelectWidget

) +); + const mockOnFocus = jest.fn(); const mockOnBlur = jest.fn(); const mockOnChange = jest.fn(); -const mockProps: WidgetProps = { +const mockBaseProps = { onFocus: mockOnFocus, onBlur: mockOnBlur, onChange: mockOnChange, registry: {} as Registry, +}; + +const mockSelectProps: WidgetProps = { + ...mockBaseProps, ...MOCK_SELECT_WIDGET, }; +const mockTreeSelectProps: WidgetProps = { + ...mockBaseProps, + ...MOCK_TREE_SELECT_WIDGET, +}; + describe('Test SelectWidget Component', () => { it('Should render select component', async () => { - render(); + render(); const selectInput = screen.getByTestId('select-widget'); + const treeSelectWidget = screen.queryByText('TreeSelectWidget'); expect(selectInput).toBeInTheDocument(); + expect(treeSelectWidget).not.toBeInTheDocument(); }); it('Should be disabled', async () => { - render(); + render(); const selectInput = await findByRole( screen.getByTestId('select-widget'), @@ -58,7 +76,7 @@ describe('Test SelectWidget Component', () => { }); it('Should call onFocus', async () => { - render(); + render(); const selectInput = screen.getByTestId('select-widget'); @@ -68,7 +86,7 @@ describe('Test SelectWidget Component', () => { }); it('Should call onBlur', async () => { - render(); + render(); const selectInput = screen.getByTestId('select-widget'); @@ -78,7 +96,7 @@ describe('Test SelectWidget Component', () => { }); it('Should call onChange', async () => { - render(); + render(); const selectInput = await findByRole( screen.getByTestId('select-widget'), @@ -97,4 +115,14 @@ describe('Test SelectWidget Component', () => { expect(mockOnChange).toHaveBeenCalledTimes(1); }); + + it('Should render TreeSelectWidget component if uiFieldType is treeSelect', async () => { + render(); + + const selectWidget = screen.queryByTestId('select-widget'); + const treeSelectWidget = screen.getByText('TreeSelectWidget'); + + expect(treeSelectWidget).toBeInTheDocument(); + expect(selectWidget).not.toBeInTheDocument(); + }); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/Form/JSONSchema/JsonSchemaWidgets/SelectWidget.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/Form/JSONSchema/JsonSchemaWidgets/SelectWidget.tsx index 84611eeef4e6..23d6cdbe4ae7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/Form/JSONSchema/JsonSchemaWidgets/SelectWidget.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/Form/JSONSchema/JsonSchemaWidgets/SelectWidget.tsx @@ -14,15 +14,18 @@ import { WidgetProps } from '@rjsf/utils'; import { Select } from 'antd'; import { capitalize } from 'lodash'; import React, { FC } from 'react'; +import TreeSelectWidget from './TreeSelectWidget'; + +const SelectWidget: FC = (props) => { + if (props.schema.uiFieldType === 'treeSelect') { + return ; + } + + const { onFocus, onBlur, onChange, ...rest } = props; -const SelectWidget: FC = ({ - onFocus, - onBlur, - onChange, - ...rest -}) => { return (