fix(ui): extend application configuration component (#23310)

* extend application configuration component

* fix tests

(cherry picked from commit 95962596e821f6581a57a61ad9eb852b9ef3c8c6)
This commit is contained in:
Karan Hotchandani 2025-09-10 16:20:17 +05:30 committed by karanh37
parent 0a585f47dd
commit 7ca86c5095
9 changed files with 91 additions and 18 deletions

View File

@ -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 AppLogo from '../AppLogo/AppLogo.component';
import AppRunsHistory from '../AppRunsHistory/AppRunsHistory.component';
import AppSchedule from '../AppSchedule/AppSchedule.component';
@ -233,13 +233,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);
@ -324,6 +330,9 @@ const AppDetails = () => {
};
const tabs = useMemo(() => {
const ApplicationConfigurationComponent =
applicationsClassBase.getApplicationConfigurationComponent();
const tabConfiguration =
appData?.appConfiguration && appData.allowConfiguration && jsonSchema
? [
@ -336,7 +345,7 @@ const AppDetails = () => {
),
key: ApplicationTabs.CONFIGURATION,
children: (
<ApplicationConfiguration
<ApplicationConfigurationComponent
appData={appData}
isLoading={loadingState.isSaveLoading}
jsonSchema={jsonSchema}

View File

@ -150,6 +150,9 @@ jest.mock('../AppSchedule/AppSchedule.component', () =>
jest.mock('./ApplicationsClassBase', () => ({
importSchema: jest.fn().mockReturnValue({ default: ['table'] }),
getJSONUISchema: jest.fn().mockReturnValue({}),
getApplicationConfigurationComponent: jest
.fn()
.mockReturnValue(() => <div>MockApplicationConfiguration</div>),
}));
jest.mock('react-router-dom', () => ({

View File

@ -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';
class ApplicationsClassBase {
public importSchema(fqn: string) {
@ -71,6 +74,14 @@ class ApplicationsClassBase {
? getScheduleOptionsFromSchedules(pipelineSchedules)
: undefined;
}
/**
* Returns the ApplicationConfiguration component to use.
* Base implementation returns the standard component.
*/
public getApplicationConfigurationComponent(): ComponentType<ApplicationConfigurationProps> {
return ApplicationConfiguration;
}
}
const applicationsClassBase = new ApplicationsClassBase();

View File

@ -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'),

View File

@ -26,6 +26,10 @@ jest.mock('../../../common/RichTextEditor/RichTextEditorPreviewerV1', () =>
jest.fn().mockImplementation(({ markdown }) => <div>{markdown}</div>)
);
jest.mock('../AppLogo/AppLogo.component', () =>
jest.fn().mockImplementation(() => <div>AppLogo</div>)
);
describe('ApplicationCard', () => {
beforeEach(() => {
jest.useFakeTimers();
@ -39,9 +43,6 @@ describe('ApplicationCard', () => {
it('renders the title correctly', () => {
render(<ApplicationCard {...props} />);
// Fast-forward until all timers have been executed
jest.runAllTimers();
expect(screen.getByText('Search Index')).toBeInTheDocument();
expect(screen.getByText('Hello World')).toBeInTheDocument();
});

View File

@ -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;
}

View File

@ -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 ScheduleInterval from '../../components/Settings/Services/AddIngestion/Steps/ScheduleInterval';
import { WorkflowExtraConfig } from '../../components/Settings/Services/AddIngestion/Steps/ScheduleInterval.interface';
import IngestionStepper from '../../components/Settings/Services/Ingestion/IngestionStepper/IngestionStepper.component';
@ -40,8 +39,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';
@ -66,6 +67,11 @@ const AppInstall = () => {
const [appConfiguration, setAppConfiguration] = useState();
const [jsonSchema, setJsonSchema] = useState<RJSFSchema>();
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(
@ -161,23 +167,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);
}
};
@ -186,6 +210,9 @@ const AppInstall = () => {
return <></>;
}
const ApplicationConfigurationComponent =
applicationsClassBase.getApplicationConfigurationComponent();
switch (activeServiceStep) {
case 1:
return (
@ -205,7 +232,7 @@ const AppInstall = () => {
case 2:
return (
<ApplicationConfiguration
<ApplicationConfigurationComponent
appData={appData}
isLoading={false}
jsonSchema={jsonSchema}

View File

@ -61,6 +61,17 @@ jest.mock(
() => ({
importSchema: jest.fn().mockResolvedValue({}),
getJSONUISchema: jest.fn().mockReturnValue({}),
getApplicationConfigurationComponent: jest
.fn()
.mockReturnValue(({ onConfigSave, onCancel }: any) => (
<div>
FormBuilder
<button onClick={() => onConfigSave({ formData: {} })}>
Submit FormBuilder
</button>
<button onClick={onCancel}>Cancel FormBuilder</button>
</div>
)),
})
);

View File

@ -17,4 +17,4 @@
"preinstall": "yarn global add node-gyp@10.0.1",
"test": "echo \"Error: no test specified\" && exit 1"
}
}
}