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

* extend application configuration component

* fix tests
This commit is contained in:
Karan Hotchandani 2025-09-10 16:20:17 +05:30 committed by GitHub
parent f3cb001d2b
commit 95962596e8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 90 additions and 17 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 { 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: (
<ApplicationConfiguration
<ApplicationConfigurationComponent
appData={appData}
isLoading={loadingState.isSaveLoading}
jsonSchema={jsonSchema}

View File

@ -154,6 +154,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';
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<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 { 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<RJSFSchema>();
const [pluginComponent, setPluginComponent] = useState<FC | null>(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 (
<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>
)),
})
);