From 95962596e821f6581a57a61ad9eb852b9ef3c8c6 Mon Sep 17 00:00:00 2001 From: Karan Hotchandani <33024356+karanh37@users.noreply.github.com> Date: Wed, 10 Sep 2025 16:20:17 +0530 Subject: [PATCH] fix(ui): extend application configuration component (#23310) * extend application configuration component * fix tests --- .../AppDetails/AppDetails.component.tsx | 17 ++++++-- .../AppDetails/AppDetails.test.tsx | 3 ++ .../AppDetails/ApplicationsClassBase.ts | 13 ++++++- .../AppRunsHistory/AppRunsHistory.test.tsx | 6 +++ .../ApplicationCard/ApplicationCard.test.tsx | 7 ++-- .../ApplicationConfiguration.tsx | 11 ++++-- .../pages/AppInstall/AppInstall.component.tsx | 39 ++++++++++++++++--- .../src/pages/AppInstall/AppInstall.test.tsx | 11 ++++++ 8 files changed, 90 insertions(+), 17 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppDetails/AppDetails.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppDetails/AppDetails.component.tsx index 02a85e0f163..a64ec6633c9 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppDetails/AppDetails.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppDetails/AppDetails.component.tsx @@ -49,6 +49,7 @@ import { ScheduleTimeline, ScheduleType, } from '../../../../generated/entity/applications/app'; +import { EntityReference } from '../../../../generated/entity/type'; import { Include } from '../../../../generated/type/include'; import { useFqn } from '../../../../hooks/useFqn'; import { @@ -70,7 +71,6 @@ import { ManageButtonItemLabel } from '../../../common/ManageButtonContentItem/M import TabsLabel from '../../../common/TabsLabel/TabsLabel.component'; import ConfirmationModal from '../../../Modals/ConfirmationModal/ConfirmationModal'; import PageLayoutV1 from '../../../PageLayoutV1/PageLayoutV1'; -import ApplicationConfiguration from '../ApplicationConfiguration/ApplicationConfiguration'; import { useApplicationsProvider } from '../ApplicationsProvider/ApplicationsProvider'; import AppLogo from '../AppLogo/AppLogo.component'; import AppRunsHistory from '../AppRunsHistory/AppRunsHistory.component'; @@ -235,13 +235,19 @@ const AppDetails = () => { ]), ]; - const onConfigSave = async (data: IChangeEvent) => { + const onConfigSave = async ( + data: IChangeEvent & { ingestionRunner?: EntityReference } + ) => { if (appData) { setLoadingState((prev) => ({ ...prev, isSaveLoading: true })); - const updatedFormData = formatFormDataForSubmit(data.formData); + + const { formData, ingestionRunner } = data; + + const updatedFormData = formatFormDataForSubmit(formData); const updatedData = { ...appData, appConfiguration: updatedFormData, + ...(ingestionRunner && { ingestionRunner }), }; const jsonPatch = compare(appData, updatedData); @@ -337,6 +343,9 @@ const AppDetails = () => { }, [appData?.name, plugins]); const tabs = useMemo(() => { + const ApplicationConfigurationComponent = + applicationsClassBase.getApplicationConfigurationComponent(); + const tabConfiguration = appData?.appConfiguration && appData.allowConfiguration && jsonSchema ? [ @@ -349,7 +358,7 @@ const AppDetails = () => { ), key: ApplicationTabs.CONFIGURATION, children: ( - jest.mock('./ApplicationsClassBase', () => ({ importSchema: jest.fn().mockReturnValue({ default: ['table'] }), getJSONUISchema: jest.fn().mockReturnValue({}), + getApplicationConfigurationComponent: jest + .fn() + .mockReturnValue(() =>
MockApplicationConfiguration
), })); jest.mock('react-router-dom', () => ({ diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppDetails/ApplicationsClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppDetails/ApplicationsClassBase.ts index c88ed0a0990..a8dc97b29d0 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppDetails/ApplicationsClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppDetails/ApplicationsClassBase.ts @@ -11,9 +11,12 @@ * limitations under the License. */ -import { FC } from 'react'; +import { ComponentType, FC } from 'react'; import { AppType } from '../../../../generated/entity/applications/app'; import { getScheduleOptionsFromSchedules } from '../../../../utils/SchedularUtils'; +import ApplicationConfiguration, { + ApplicationConfigurationProps, +} from '../ApplicationConfiguration/ApplicationConfiguration'; import { AppPlugin } from '../plugins/AppPlugin'; class ApplicationsClassBase { @@ -84,6 +87,14 @@ class ApplicationsClassBase { ? getScheduleOptionsFromSchedules(pipelineSchedules) : undefined; } + + /** + * Returns the ApplicationConfiguration component to use. + * Base implementation returns the standard component. + */ + public getApplicationConfigurationComponent(): ComponentType { + return ApplicationConfiguration; + } } const applicationsClassBase = new ApplicationsClassBase(); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppRunsHistory/AppRunsHistory.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppRunsHistory/AppRunsHistory.test.tsx index 8ac7671dbab..97628f98317 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppRunsHistory/AppRunsHistory.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppRunsHistory/AppRunsHistory.test.tsx @@ -41,6 +41,11 @@ let mockGetApplicationRuns = jest.fn().mockReturnValue({ const mockShowErrorToast = jest.fn(); const mockNavigate = jest.fn(); +jest.mock('../../../../constants/LeftSidebar.constants', () => ({ + SIDEBAR_NESTED_KEYS: {}, + SIDEBAR_LIST: [], +})); + jest.mock('../../../../utils/EntityUtils', () => ({ getEntityName: jest.fn().mockReturnValue('username'), })); @@ -105,6 +110,7 @@ jest.mock('../../../../utils/date-time/DateTimeUtils', () => ({ return 'formatDateTime'; }), + getCurrentMillis: jest.fn().mockReturnValue(1234567890000), getEpochMillisForPastDays: jest.fn().mockReturnValue('startDay'), getIntervalInMilliseconds: jest.fn().mockReturnValue('interval'), formatDuration: jest.fn().mockReturnValue('formatDuration'), diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/ApplicationCard/ApplicationCard.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/ApplicationCard/ApplicationCard.test.tsx index 4757e8c24b9..604fd9d900b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/ApplicationCard/ApplicationCard.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/ApplicationCard/ApplicationCard.test.tsx @@ -26,6 +26,10 @@ jest.mock('../../../common/RichTextEditor/RichTextEditorPreviewerV1', () => jest.fn().mockImplementation(({ markdown }) =>
{markdown}
) ); +jest.mock('../AppLogo/AppLogo.component', () => + jest.fn().mockImplementation(() =>
AppLogo
) +); + describe('ApplicationCard', () => { beforeEach(() => { jest.useFakeTimers(); @@ -39,9 +43,6 @@ describe('ApplicationCard', () => { it('renders the title correctly', () => { render(); - // Fast-forward until all timers have been executed - jest.runAllTimers(); - expect(screen.getByText('Search Index')).toBeInTheDocument(); expect(screen.getByText('Hello World')).toBeInTheDocument(); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/ApplicationConfiguration/ApplicationConfiguration.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/ApplicationConfiguration/ApplicationConfiguration.tsx index 1f5b5352663..eebc4617c1c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/ApplicationConfiguration/ApplicationConfiguration.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/ApplicationConfiguration/ApplicationConfiguration.tsx @@ -17,18 +17,23 @@ import { isEmpty } from 'lodash'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { ServiceCategory } from '../../../../enums/service.enum'; -import { App } from '../../../../generated/entity/applications/app'; +import { + App, + EntityReference, +} from '../../../../generated/entity/applications/app'; import { AppMarketPlaceDefinition } from '../../../../generated/entity/applications/marketplace/appMarketPlaceDefinition'; import FormBuilder from '../../../common/FormBuilder/FormBuilder'; import ResizablePanels from '../../../common/ResizablePanels/ResizablePanels'; import ServiceDocPanel from '../../../common/ServiceDocPanel/ServiceDocPanel'; import applicationsClassBase from '../AppDetails/ApplicationsClassBase'; -interface ApplicationConfigurationProps { +export interface ApplicationConfigurationProps { appData: App | AppMarketPlaceDefinition; isLoading: boolean; jsonSchema: RJSFSchema; - onConfigSave: (data: IChangeEvent) => void; + onConfigSave: ( + data: IChangeEvent & { ingestionRunner?: EntityReference } + ) => void; onCancel?: () => void; } diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/AppInstall/AppInstall.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/AppInstall/AppInstall.component.tsx index 21d044efe8e..1cdb4829d9a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/AppInstall/AppInstall.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/AppInstall/AppInstall.component.tsx @@ -26,7 +26,6 @@ import { default as applicationsClassBase, } from '../../components/Settings/Applications/AppDetails/ApplicationsClassBase'; import AppInstallVerifyCard from '../../components/Settings/Applications/AppInstallVerifyCard/AppInstallVerifyCard.component'; -import ApplicationConfiguration from '../../components/Settings/Applications/ApplicationConfiguration/ApplicationConfiguration'; import { AppPlugin } from '../../components/Settings/Applications/plugins/AppPlugin'; import ScheduleInterval from '../../components/Settings/Services/AddIngestion/Steps/ScheduleInterval'; import { WorkflowExtraConfig } from '../../components/Settings/Services/AddIngestion/Steps/ScheduleInterval.interface'; @@ -41,8 +40,10 @@ import { } from '../../generated/entity/applications/createAppRequest'; import { AppMarketPlaceDefinition, + AppType, ScheduleType, } from '../../generated/entity/applications/marketplace/appMarketPlaceDefinition'; +import { EntityReference } from '../../generated/entity/type'; import { useFqn } from '../../hooks/useFqn'; import { installApplication } from '../../rest/applicationAPI'; import { getMarketPlaceApplicationByFqn } from '../../rest/applicationMarketPlaceAPI'; @@ -68,6 +69,11 @@ const AppInstall = () => { const [jsonSchema, setJsonSchema] = useState(); const [pluginComponent, setPluginComponent] = useState(null); const { config, getResourceLimit } = useLimitStore(); + const [selectedIngestionRunner, setSelectedIngestionRunner] = useState< + EntityReference | undefined + >(undefined); + const shouldShowIngestionRunner = + appData?.appType === AppType.External && appData?.supportsIngestionRunner; const { pipelineSchedules } = config?.limits?.config.featureLimits.find( @@ -177,23 +183,41 @@ const AppInstall = () => { name: fqn, description: appData?.description, displayName: appData?.displayName, + ingestionRunner: shouldShowIngestionRunner + ? selectedIngestionRunner + : undefined, }; installApp(data); }; - const onSaveConfiguration = (data: IChangeEvent) => { - const updatedFormData = formatFormDataForSubmit(data.formData); + const onSaveConfiguration = ( + data: IChangeEvent & { ingestionRunner?: EntityReference } + ) => { + const { formData, ingestionRunner } = data; + + const updatedFormData = formatFormDataForSubmit(formData); setAppConfiguration(updatedFormData); + const ingestionRunnerRef = ingestionRunner + ? { + id: ingestionRunner.id, + type: 'ingestionRunner', + name: ingestionRunner.name, + fullyQualifiedName: ingestionRunner.fullyQualifiedName, + } + : undefined; + setSelectedIngestionRunner(ingestionRunnerRef); + if (appData?.scheduleType !== ScheduleType.NoSchedule) { setActiveServiceStep(3); } else { - const data: CreateAppRequest = { + const requestData: CreateAppRequest = { appConfiguration: updatedFormData, name: fqn, description: appData?.description, displayName: appData?.displayName, + ...(ingestionRunnerRef ? { ingestionRunner: ingestionRunnerRef } : {}), }; - installApp(data); + installApp(requestData); } }; @@ -202,6 +226,9 @@ const AppInstall = () => { return <>; } + const ApplicationConfigurationComponent = + applicationsClassBase.getApplicationConfigurationComponent(); + switch (activeServiceStep) { case 1: return ( @@ -221,7 +248,7 @@ const AppInstall = () => { case 2: return ( - ({ importSchema: jest.fn().mockResolvedValue({}), getJSONUISchema: jest.fn().mockReturnValue({}), + getApplicationConfigurationComponent: jest + .fn() + .mockReturnValue(({ onConfigSave, onCancel }: any) => ( +
+ FormBuilder + + +
+ )), }) );