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 index 88d1746ad0a9..3a3140fc9aa6 100644 --- 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 @@ -15,6 +15,7 @@ import { expect, test } from '@playwright/test'; import * as fs from 'fs'; import * as path from 'path'; import { PLAYWRIGHT_INGESTION_TAG_OBJ } from '../../constant/config'; +import { SERVICE_TYPE } from '../../constant/service'; import { CERT_FILE, lookerFormDetails, @@ -23,9 +24,11 @@ import { supersetFormDetails3, supersetFormDetails4, } from '../../constant/serviceForm'; +import { MessagingServiceClass } from '../../support/entity/service/MessagingServiceClass'; import { UserClass } from '../../support/user/UserClass'; import { createNewPage, redirectToHomePage, uuid } from '../../utils/common'; import { waitForAllLoadersToDisappear } from '../../utils/entity'; +import { visitServiceDetailsPage } from '../../utils/service'; import { fillSupersetFormDetails } from '../../utils/serviceFormUtils'; const SERVICE_NAMES = { @@ -453,5 +456,103 @@ test.describe( ); }); }); + + // Regression coverage for issue #25434: + // Clearing `schemaRegistryTopicSuffixName` on a Kafka connection must + // send an empty string to the backend instead of dropping the field, + // so the cleared value is persisted on reload. + test.describe('Kafka', () => { + const kafkaService = new MessagingServiceClass( + `pw-kafka-empty-suffix-${uuid()}` + ); + + test.beforeAll( + 'Create Kafka service with suffix set', + async ({ browser }) => { + const { apiContext, afterAction } = await createNewPage(browser); + await kafkaService.create(apiContext); + await kafkaService.patch(apiContext, [ + { + op: 'add', + path: '/connection/config/schemaRegistryURL', + value: 'http://localhost:8081', + }, + { + op: 'add', + path: '/connection/config/schemaRegistryTopicSuffixName', + value: '-value', + }, + ]); + await afterAction(); + } + ); + + test.afterAll('Cleanup Kafka service', async ({ browser }) => { + const { apiContext, afterAction } = await createNewPage(browser); + await kafkaService.delete(apiContext); + await afterAction(); + }); + + test('should persist empty schemaRegistryTopicSuffixName when the field is cleared', async ({ + page, + }) => { + await visitServiceDetailsPage( + page, + { + name: kafkaService.entity.name, + type: SERVICE_TYPE.Messaging, + }, + false, + false + ); + + await page.getByRole('tab', { name: 'Connection' }).click(); + await page.getByTestId('edit-connection-button').click(); + await waitForAllLoadersToDisappear(page); + + const suffixInput = page.locator( + String.raw`#root\/schemaRegistryTopicSuffixName` + ); + + await expect(suffixInput).toHaveValue('-value'); + await suffixInput.clear(); + await expect(suffixInput).toHaveValue(''); + + const patchResponse = page.waitForResponse( + (response) => + response.url().includes('/api/v1/services/messagingServices') && + response.request().method() === 'PATCH' + ); + + await page.getByTestId('submit-btn').click(); + await page.getByTestId('submit-btn').click(); + + const patch = await patchResponse; + const patchBody = patch.request().postDataJSON() as Array<{ + op: string; + path: string; + value?: unknown; + }>; + + const suffixOp = patchBody.find( + (op) => op.path === '/connection/config/schemaRegistryTopicSuffixName' + ); + + // The fix must send an explicit empty string, not drop the field + // (which would leave the stale "-value" on the server). + expect(suffixOp).toBeDefined(); + expect(suffixOp?.value).toBe(''); + + await waitForAllLoadersToDisappear(page); + + // Reopen the edit form and verify the cleared value persisted. + await page.getByTestId('edit-connection-button').click(); + await waitForAllLoadersToDisappear(page); + + await expect( + page.locator(String.raw`#root\/schemaRegistryTopicSuffixName`) + ).toHaveValue(''); + }); + }); } ); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/MessagingServiceUtils.test.ts b/openmetadata-ui/src/main/resources/ui/src/utils/MessagingServiceUtils.test.ts new file mode 100644 index 000000000000..9cf1fa9b5ac3 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/utils/MessagingServiceUtils.test.ts @@ -0,0 +1,78 @@ +/* + * 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. + */ + +jest.mock( + '../jsons/connectionSchemas/connections/messaging/kafkaConnection.json', + () => ({}), + { virtual: true } +); +jest.mock( + '../jsons/connectionSchemas/connections/messaging/redpandaConnection.json', + () => ({}), + { virtual: true } +); +jest.mock( + '../jsons/connectionSchemas/connections/messaging/customMessagingConnection.json', + () => ({}), + { virtual: true } +); +jest.mock( + '../jsons/connectionSchemas/connections/messaging/kinesisConnection.json', + () => ({}), + { virtual: true } +); +jest.mock( + '../jsons/connectionSchemas/connections/messaging/pubSubConnection.json', + () => ({}), + { virtual: true } +); + +import { COMMON_UI_SCHEMA } from '../constants/ServiceUISchema.constant'; +import { MessagingServiceType } from '../generated/entity/services/messagingService'; +import { getMessagingConfig } from './MessagingServiceUtils'; + +describe('MessagingServiceUtils', () => { + it('Kafka uiSchema should include ui:emptyValue for schemaRegistryTopicSuffixName', () => { + const config = getMessagingConfig(MessagingServiceType.Kafka); + + expect(config.uiSchema).toMatchObject({ + ...COMMON_UI_SCHEMA, + schemaRegistryTopicSuffixName: { + 'ui:emptyValue': '', + }, + }); + }); + + it('Redpanda uiSchema should include ui:emptyValue for schemaRegistryTopicSuffixName', () => { + const config = getMessagingConfig(MessagingServiceType.Redpanda); + + expect(config.uiSchema).toMatchObject({ + ...COMMON_UI_SCHEMA, + schemaRegistryTopicSuffixName: { + 'ui:emptyValue': '', + }, + }); + }); + + it('non-broker services should not include schemaRegistryTopicSuffixName uiSchema', () => { + const config = getMessagingConfig(MessagingServiceType.Kinesis); + + expect(config.uiSchema).not.toHaveProperty('schemaRegistryTopicSuffixName'); + }); + + it('getMessagingConfig should return only common UI schema for invalid types', () => { + const config = getMessagingConfig('' as MessagingServiceType); + + expect(config.uiSchema).toEqual({ ...COMMON_UI_SCHEMA }); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/MessagingServiceUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/MessagingServiceUtils.ts index dced426db1f3..0977fff44f77 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/MessagingServiceUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/MessagingServiceUtils.ts @@ -34,6 +34,12 @@ export const getBrokers = (config: MessagingConnection['config']) => { return isUndefined(retVal) ? '--' : retVal; }; +const SCHEMA_REGISTRY_SUFFIX_UI_SCHEMA = { + schemaRegistryTopicSuffixName: { + 'ui:emptyValue': '', + }, +}; + export const getMessagingConfig = (type: MessagingServiceType) => { let schema = {}; const uiSchema = { ...COMMON_UI_SCHEMA }; @@ -41,11 +47,13 @@ export const getMessagingConfig = (type: MessagingServiceType) => { switch (type) { case MessagingServiceType.Kafka: schema = kafkaConnection; + Object.assign(uiSchema, SCHEMA_REGISTRY_SUFFIX_UI_SCHEMA); break; case MessagingServiceType.Redpanda: schema = redpandaConnection; + Object.assign(uiSchema, SCHEMA_REGISTRY_SUFFIX_UI_SCHEMA); break;