diff --git a/openmetadata-spec/src/main/resources/json/schema/entity/services/connections/dashboard/supersetConnection.json b/openmetadata-spec/src/main/resources/json/schema/entity/services/connections/dashboard/supersetConnection.json index fdb70b85ce2..c4f33e8f65d 100644 --- a/openmetadata-spec/src/main/resources/json/schema/entity/services/connections/dashboard/supersetConnection.json +++ b/openmetadata-spec/src/main/resources/json/schema/entity/services/connections/dashboard/supersetConnection.json @@ -41,10 +41,7 @@ { "$ref": "../database/mysqlConnection.json" } - ], - "default": { - "provider": "db" - } + ] }, "dashboardFilterPattern": { "description": "Regex to exclude or include dashboards that matches the pattern.", diff --git a/openmetadata-ui/src/main/resources/ui/playwright/constant/serviceForm.ts b/openmetadata-ui/src/main/resources/ui/playwright/constant/serviceForm.ts new file mode 100644 index 00000000000..9061a29b00d --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/playwright/constant/serviceForm.ts @@ -0,0 +1,56 @@ +/* + * Copyright 2025 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 { SupersetFormType } from '../support/interfaces/ServiceForm.interface'; + +export const supersetFormDetails1: SupersetFormType = { + hostPort: 'http://localhost:8088', + connectionType: 'SupersetApiConnection', + connection: { + provider: 'db', + username: 'test-user-1', + password: 'test-password-1', + }, +}; + +export const supersetFormDetails2: SupersetFormType = { + hostPort: 'http://localhost:8085', + connectionType: 'SupersetApiConnection', + connection: { + provider: 'ldap', + username: 'test-user-2', + password: 'test-password-2', + }, +}; + +export const supersetFormDetails3: SupersetFormType = { + hostPort: 'http://localhost:8086', + connectionType: 'PostgresConnection', + connection: { + username: 'test-user-3', + password: 'test-password-3', + hostPort: 'http://localhost:5432', + database: 'test_db', + scheme: 'postgresql+psycopg2', + }, +}; + +export const supersetFormDetails4: SupersetFormType = { + hostPort: 'http://localhost:8086', + connectionType: 'MysqlConnection', + connection: { + username: 'test-user-3', + password: 'test-password-3', + hostPort: 'http://localhost:5432', + scheme: 'mysql+pymysql', + }, +}; diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/ServiceForm.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/ServiceForm.spec.ts new file mode 100644 index 00000000000..c9ddcf84e11 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/ServiceForm.spec.ts @@ -0,0 +1,166 @@ +/* + * 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 { expect } from '@playwright/test'; +import { + supersetFormDetails1, + supersetFormDetails2, + supersetFormDetails3, + supersetFormDetails4, +} from '../../constant/serviceForm'; +import { redirectToHomePage } from '../../utils/common'; +import { waitForAllLoadersToDisappear } from '../../utils/entity'; +import { fillSupersetFormDetails } from '../../utils/serviceFormUtils'; +import { test } from '../fixtures/pages'; + +test.describe('Service form functionality', async () => { + test.beforeEach(async ({ page }) => { + await redirectToHomePage(page); + }); + + test.describe('Superset', () => { + test('Verify form selects are working properly', async ({ page }) => { + await page.goto('/dashboardServices/add-service'); + await waitForAllLoadersToDisappear(page); + await page.click(`[data-testid="Superset"]`); + await page.click('[data-testid="next-button"]'); + + await page.fill('[data-testid="service-name"]', 'test-superset'); + await page.click('[data-testid="next-button"]'); + + // Fill superset form details - 1 + await fillSupersetFormDetails({ page, ...supersetFormDetails1 }); + + const testConnectionResponse1 = page.waitForResponse( + 'api/v1/automations/workflows' + ); + + await page.getByTestId('test-connection-btn').click(); + + const testConnection1 = await (await testConnectionResponse1).json(); + + // Verify form details submission - 1 + expect(testConnection1.request.connection.config.hostPort).toEqual( + supersetFormDetails1.hostPort + ); + expect( + testConnection1.request.connection.config.connection.username + ).toEqual(supersetFormDetails1.connection.username); + expect( + testConnection1.request.connection.config.connection.provider + ).toEqual(supersetFormDetails1.connection.provider); + + await page + .getByTestId('test-connection-modal') + .getByRole('button', { name: 'OK' }) + .click(); + + await page.waitForSelector('[data-testid="test-connection-modal"]', { + state: 'hidden', + }); + + // Fill superset form details - 2 + await fillSupersetFormDetails({ page, ...supersetFormDetails2 }); + + const testConnectionResponse2 = page.waitForResponse( + 'api/v1/automations/workflows' + ); + + await page.getByTestId('test-connection-btn').click(); + + const testConnection2 = await (await testConnectionResponse2).json(); + + // Verify form details submission - 2 + expect(testConnection2.request.connection.config.hostPort).toEqual( + supersetFormDetails2.hostPort + ); + expect( + testConnection2.request.connection.config.connection.username + ).toEqual(supersetFormDetails2.connection.username); + expect( + testConnection2.request.connection.config.connection.provider + ).toEqual(supersetFormDetails2.connection.provider); + + await page + .getByTestId('test-connection-modal') + .getByRole('button', { name: 'OK' }) + .click(); + + await page.waitForSelector('[data-testid="test-connection-modal"]', { + state: 'hidden', + }); + + // Fill superset form details - 3 + await fillSupersetFormDetails({ page, ...supersetFormDetails3 }); + + const testConnectionResponse3 = page.waitForResponse( + 'api/v1/automations/workflows' + ); + + await page.getByTestId('test-connection-btn').click(); + + const testConnection3 = await (await testConnectionResponse3).json(); + + // Verify form details submission - 3 + expect(testConnection3.request.connection.config.hostPort).toEqual( + supersetFormDetails3.hostPort + ); + expect( + testConnection3.request.connection.config.connection.username + ).toEqual(supersetFormDetails3.connection.username); + expect( + testConnection3.request.connection.config.connection.hostPort + ).toEqual(supersetFormDetails3.connection.hostPort); + expect( + testConnection3.request.connection.config.connection.database + ).toEqual(supersetFormDetails3.connection.database); + expect( + testConnection3.request.connection.config.connection.scheme + ).toEqual(supersetFormDetails3.connection.scheme); + + await page + .getByTestId('test-connection-modal') + .getByRole('button', { name: 'OK' }) + .click(); + + await page.waitForSelector('[data-testid="test-connection-modal"]', { + state: 'hidden', + }); + + // Fill superset form details - 4 + await fillSupersetFormDetails({ page, ...supersetFormDetails4 }); + + const testConnectionResponse4 = page.waitForResponse( + 'api/v1/automations/workflows' + ); + + await page.getByTestId('test-connection-btn').click(); + + const testConnection4 = await (await testConnectionResponse4).json(); + + // Verify form details submission - 4 + expect(testConnection4.request.connection.config.hostPort).toEqual( + supersetFormDetails4.hostPort + ); + expect( + testConnection4.request.connection.config.connection.username + ).toEqual(supersetFormDetails4.connection.username); + expect( + testConnection4.request.connection.config.connection.hostPort + ).toEqual(supersetFormDetails4.connection.hostPort); + expect( + testConnection4.request.connection.config.connection.scheme + ).toEqual(supersetFormDetails4.connection.scheme); + }); + }); +}); 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 index e5ae9db3c27..a576ad4f601 100644 --- 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 @@ -152,7 +152,9 @@ test('Search Index Application', async ({ page }) => { await clickOutside(page); await page.locator('[for="root/searchIndexMappingLanguage"]').click(); - await page.getByTestId('select-widget').click(); + await page + .getByTestId('select-widget-root/searchIndexMappingLanguage') + .click(); await expect(page.getByTestId('select-option-JP')).toBeVisible(); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/ingestion/AirflowIngestionClass.ts b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/ingestion/AirflowIngestionClass.ts index a0101b37969..2a36085a9e2 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/ingestion/AirflowIngestionClass.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/ingestion/AirflowIngestionClass.ts @@ -53,8 +53,12 @@ class AirflowIngestionClass extends ServiceBaseClass { await page.locator('#root\\/hostPort').fill(airflowHostPort); await page - .locator('#root\\/connection__oneof_select') - .selectOption('BackendConnection'); + .getByTestId('select-widget-root/connection__oneof_select') + .getByRole('combobox') + .click({ force: true }); + await page.click( + '.ant-select-dropdown:visible [title="BackendConnection"]' + ); } async deleteService(page: Page) { diff --git a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/ingestion/BigQueryIngestionClass.ts b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/ingestion/BigQueryIngestionClass.ts index e73e7eb2855..1f190e63920 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/ingestion/BigQueryIngestionClass.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/ingestion/BigQueryIngestionClass.ts @@ -65,9 +65,12 @@ class BigQueryIngestionClass extends ServiceBaseClass { const projectIdTaxonomy = process.env.PLAYWRIGHT_BQ_PROJECT_ID_TAXONOMY ?? ''; - await page.selectOption( - '#root\\/credentials\\/gcpConfig__oneof_select', - 'GCP Credentials Values' + await page + .getByTestId('select-widget-root/credentials/gcpConfig__oneof_select') + .getByRole('combobox') + .click({ force: true }); + await page.click( + '.ant-select-dropdown:visible [title="GCP Credentials Values"]' ); await page.fill('#root\\/credentials\\/gcpConfig\\/projectId', projectId); await checkServiceFieldSectionHighlighting(page, 'projectId'); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/ingestion/RedshiftWithDBTIngestionClass.ts b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/ingestion/RedshiftWithDBTIngestionClass.ts index f2ed8bcee58..a1aec03af44 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/ingestion/RedshiftWithDBTIngestionClass.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/ingestion/RedshiftWithDBTIngestionClass.ts @@ -131,10 +131,11 @@ class RedshiftWithDBTIngestionClass extends ServiceBaseClass { await page.click('[data-menu-id*="dbt"]'); await page.waitForSelector('#root\\/dbtConfigSource__oneof_select'); - await page.selectOption( - '#root\\/dbtConfigSource__oneof_select', - 'DBT S3 Config' - ); + await page + .getByTestId('select-widget-root/dbtConfigSource__oneof_select') + .getByRole('combobox') + .click({ force: true }); + await page.click('.ant-select-dropdown:visible [title="DBT S3 Config"]'); await page.fill( '#root\\/dbtConfigSource\\/dbtSecurityConfig\\/awsAccessKeyId', process.env.PLAYWRIGHT_S3_STORAGE_ACCESS_KEY_ID ?? '' diff --git a/openmetadata-ui/src/main/resources/ui/playwright/support/interfaces/ServiceForm.interface.ts b/openmetadata-ui/src/main/resources/ui/playwright/support/interfaces/ServiceForm.interface.ts new file mode 100644 index 00000000000..3cdd35f3ade --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/playwright/support/interfaces/ServiceForm.interface.ts @@ -0,0 +1,30 @@ +/* + * Copyright 2025 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 { Page } from '@playwright/test'; + +export interface SupersetFormType { + hostPort: string; + connectionType: string; + connection: { + username: string; + password: string; + provider?: string; + hostPort?: string; + database?: string; + scheme?: string; + }; +} + +export interface FillSupersetFormProps extends SupersetFormType { + page: Page; +} diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/serviceFormUtils.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/serviceFormUtils.ts new file mode 100644 index 00000000000..b380971c905 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/serviceFormUtils.ts @@ -0,0 +1,86 @@ +/* + * Copyright 2025 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 { FillSupersetFormProps } from '../support/interfaces/ServiceForm.interface'; + +export const fillSupersetFormDetails = async ({ + page, + hostPort, + connectionType, + connection: { + username, + password, + provider, + hostPort: connectionHostPort, + database, + }, +}: FillSupersetFormProps) => { + await page.locator('#root\\/hostPort').clear(); + await page.fill('#root\\/hostPort', hostPort); + + if (connectionType === 'SupersetApiConnection') { + await page + .getByTestId('select-widget-root/connection__oneof_select') + .getByRole('combobox') + .click({ force: true }); + await page.click( + `.ant-select-dropdown:visible [title="${connectionType}"]` + ); + + if (provider) { + await page + .getByTestId('select-widget-root/connection/provider') + .getByRole('combobox') + .click({ force: true }); + await page.click(`.ant-select-dropdown:visible [title="${provider}"]`); + } + } else if ( + connectionType === 'PostgresConnection' || + connectionType === 'MysqlConnection' + ) { + await page + .getByTestId('select-widget-root/connection__oneof_select') + .getByRole('combobox') + .click({ force: true }); + await page.click( + `.ant-select-dropdown:visible [title="${connectionType}"]` + ); + + if (connectionHostPort) { + await page.locator('#root\\/connection\\/hostPort').clear(); + await page.fill('#root\\/connection\\/hostPort', connectionHostPort, { + force: true, + }); + } + + if (database) { + await page.locator('#root\\/connection\\/database').clear(); + await page.fill('#root\\/connection\\/database', database, { + force: true, + }); + } + } + + await page.locator('#root\\/connection\\/username').clear(); + await page.fill('#root\\/connection\\/username', username, { force: true }); + if (connectionType === 'SupersetApiConnection') { + await page.locator('#root\\/connection\\/password').clear(); + await page.fill('#root\\/connection\\/password', password, { + force: true, + }); + } else { + await page.locator('#root\\/connection\\/authType\\/password').clear(); + await page.fill('#root\\/connection\\/authType\\/password', password, { + force: true, + }); + } +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppRunsHistory/AppRunsHistory.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppRunsHistory/AppRunsHistory.component.tsx index acf30c4de2d..5404cb44eb0 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppRunsHistory/AppRunsHistory.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppRunsHistory/AppRunsHistory.component.tsx @@ -460,6 +460,7 @@ const AppRunsHistory = forwardRef( } width={800}> { it('Should render select component', async () => { render(); - const selectInput = screen.getByTestId('select-widget'); + const selectInput = screen.getByTestId( + 'select-widget-root/searchIndexMappingLanguage' + ); const treeSelectWidget = screen.queryByText('TreeSelectWidget'); expect(selectInput).toBeInTheDocument(); @@ -67,7 +69,7 @@ describe('Test SelectWidget Component', () => { render(); const selectInput = await findByRole( - screen.getByTestId('select-widget'), + screen.getByTestId('select-widget-root/searchIndexMappingLanguage'), 'combobox' ); @@ -77,7 +79,9 @@ describe('Test SelectWidget Component', () => { it('Should call onFocus', async () => { render(); - const selectInput = screen.getByTestId('select-widget'); + const selectInput = screen.getByTestId( + 'select-widget-root/searchIndexMappingLanguage' + ); fireEvent.focus(selectInput); @@ -87,7 +91,9 @@ describe('Test SelectWidget Component', () => { it('Should call onBlur', async () => { render(); - const selectInput = screen.getByTestId('select-widget'); + const selectInput = screen.getByTestId( + 'select-widget-root/searchIndexMappingLanguage' + ); fireEvent.blur(selectInput); @@ -98,7 +104,7 @@ describe('Test SelectWidget Component', () => { render(); const selectInput = await findByRole( - screen.getByTestId('select-widget'), + screen.getByTestId('select-widget-root/searchIndexMappingLanguage'), 'combobox' ); @@ -118,7 +124,9 @@ describe('Test SelectWidget Component', () => { it('Should render TreeSelectWidget component if uiFieldType is treeSelect', async () => { render(); - const selectWidget = screen.queryByTestId('select-widget'); + const selectWidget = screen.queryByTestId( + 'select-widget-root/searchIndexMappingLanguage' + ); const treeSelectWidget = screen.getByText('TreeSelectWidget'); expect(treeSelectWidget).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 ac306f86563..b95412bc132 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 @@ -12,7 +12,6 @@ */ import { WidgetProps } from '@rjsf/utils'; import { Select } from 'antd'; -import { capitalize } from 'lodash'; import { FC } from 'react'; import { getPopupContainer } from '../../../../../utils/formUtils'; import TreeSelectWidget from './TreeSelectWidget'; @@ -29,7 +28,7 @@ const SelectWidget: FC = (props) => { allowClear autoFocus={rest.autofocus} className="d-block w-full" - data-testid="select-widget" + data-testid={`select-widget-${rest.id}`} disabled={rest.disabled} getPopupContainer={getPopupContainer} id={rest.id} @@ -45,11 +44,29 @@ const SelectWidget: FC = (props) => { data-testid={`select-option-${option.label}`} key={option.value} value={option.value}> - {capitalize(option.label)} + {rest.capitalizeOptionLabel + ? typeof option.label === 'string' + ? option.label.charAt(0).toUpperCase() + option.label.slice(1) + : option.label + : option.label} ))} ); }; -export default SelectWidget; +function withSelectWidget(WrappedComponent: FC) { + return function SelectWidget({ + capitalizeOptionLabel = false, + ...props + }: WidgetProps & { capitalizeOptionLabel?: boolean }) { + return ( + + ); + }; +} + +export default withSelectWidget(SelectWidget); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/FormBuilder/FormBuilder.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/FormBuilder/FormBuilder.tsx index 51d2ee67874..3328b2f39f3 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/FormBuilder/FormBuilder.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/FormBuilder/FormBuilder.tsx @@ -13,6 +13,7 @@ import { CheckOutlined } from '@ant-design/icons'; import Form, { FormProps, IChangeEvent } from '@rjsf/core'; +import { WidgetProps } from '@rjsf/utils'; import { Button } from 'antd'; import classNames from 'classnames'; import { LoadingState } from 'Models'; @@ -42,6 +43,7 @@ export interface Props extends FormProps { status?: LoadingState; onCancel?: () => void; useSelectWidget?: boolean; + capitalizeOptionLabel?: boolean; } const FormBuilder = forwardRef( @@ -60,6 +62,7 @@ const FormBuilder = forwardRef( uiSchema, onFocus, useSelectWidget = false, + capitalizeOptionLabel = false, children, ...props }, @@ -78,7 +81,14 @@ const FormBuilder = forwardRef( autoComplete: AsyncSelectWidget, queryBuilder: QueryBuilderWidget, code: CodeWidget, - ...(useSelectWidget && { SelectWidget: SelectWidget }), + ...(useSelectWidget && { + SelectWidget: (props: WidgetProps) => ( + + ), + }), }; const handleCancel = () => {