format cron from quartz to unix in applications (#15038)

* fix initial value cron value in applications

* added unit test for cron utils functions

* remove quartz cron for applicaion from ui install and edit

* changes as per comments and fix unit test

* fix unit test

* fix user cypress failure on click
This commit is contained in:
Ashish Gupta 2024-02-09 15:17:14 +05:30 committed by GitHub
parent 8bea265f0a
commit 847b2d8bc9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 91 additions and 60 deletions

View File

@ -85,8 +85,7 @@ export const softDeleteUser = (username: string, displayName: string) => {
verifyResponseStatusCode('@searchUser', 200); verifyResponseStatusCode('@searchUser', 200);
// Click on delete button // Click on delete button
cy.get(`[data-testid="delete-user-btn-${username}"]`); cy.get(`[data-testid="delete-user-btn-${username}"]`).click();
cy.get(':nth-child(4) > .ant-space > .ant-space-item > .ant-btn').click();
// Soft deleting the user // Soft deleting the user
cy.get('[data-testid="soft-delete"]').click(); cy.get('[data-testid="soft-delete"]').click();
cy.get('[data-testid="confirmation-text-input"]').type('DELETE'); cy.get('[data-testid="confirmation-text-input"]').type('DELETE');

View File

@ -45,7 +45,6 @@ export interface TestSuiteSchedulerProps {
initialData?: string; initialData?: string;
onSubmit: (repeatFrequency: string) => void; onSubmit: (repeatFrequency: string) => void;
onCancel: () => void; onCancel: () => void;
isQuartzCron?: boolean;
buttonProps?: { buttonProps?: {
okText: string; okText: string;
cancelText: string; cancelText: string;

View File

@ -11,7 +11,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { Button, Col, Row, Space } from 'antd'; import { Button, Col, Form, Row, Space } from 'antd';
import { t } from 'i18next'; import { t } from 'i18next';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import CronEditor from '../../common/CronEditor/CronEditor'; import CronEditor from '../../common/CronEditor/CronEditor';
@ -22,7 +22,6 @@ const TestSuiteScheduler: React.FC<TestSuiteSchedulerProps> = ({
buttonProps, buttonProps,
onCancel, onCancel,
onSubmit, onSubmit,
isQuartzCron = false,
includePeriodOptions, includePeriodOptions,
}) => { }) => {
const [repeatFrequency, setRepeatFrequency] = useState<string | undefined>( const [repeatFrequency, setRepeatFrequency] = useState<string | undefined>(
@ -38,12 +37,13 @@ const TestSuiteScheduler: React.FC<TestSuiteSchedulerProps> = ({
return ( return (
<Row gutter={[16, 32]}> <Row gutter={[16, 32]}>
<Col span={24}> <Col span={24}>
<Form data-testid="schedule-container">
<CronEditor <CronEditor
includePeriodOptions={includePeriodOptions} includePeriodOptions={includePeriodOptions}
isQuartzCron={isQuartzCron}
value={repeatFrequency} value={repeatFrequency}
onChange={(value: string) => setRepeatFrequency(value)} onChange={(value: string) => setRepeatFrequency(value)}
/> />
</Form>
</Col> </Col>
<Col span={24}> <Col span={24}>
<Space className="w-full justify-end" size={16}> <Space className="w-full justify-end" size={16}>

View File

@ -348,7 +348,6 @@ const AppDetails = () => {
{appData && ( {appData && (
<AppSchedule <AppSchedule
appData={appData} appData={appData}
onCancel={onBrowseAppsClick}
onDemandTrigger={onDemandTrigger} onDemandTrigger={onDemandTrigger}
onDeployTrigger={onDeployTrigger} onDeployTrigger={onDeployTrigger}
onSave={onAppScheduleSave} onSave={onAppScheduleSave}

View File

@ -124,17 +124,14 @@ jest.mock('../AppRunsHistory/AppRunsHistory.component', () =>
jest.mock('../AppSchedule/AppSchedule.component', () => jest.mock('../AppSchedule/AppSchedule.component', () =>
jest jest
.fn() .fn()
.mockImplementation( .mockImplementation(({ onSave, onDemandTrigger, onDeployTrigger }) => (
({ onCancel, onSave, onDemandTrigger, onDeployTrigger }) => (
<> <>
AppSchedule AppSchedule
<button onClick={onCancel}>Cancel AppSchedule</button>
<button onClick={onSave}>Save AppSchedule</button> <button onClick={onSave}>Save AppSchedule</button>
<button onClick={onDemandTrigger}>DemandTrigger AppSchedule</button> <button onClick={onDemandTrigger}>DemandTrigger AppSchedule</button>
<button onClick={onDeployTrigger}>DeployTrigger AppSchedule</button> <button onClick={onDeployTrigger}>DeployTrigger AppSchedule</button>
</> </>
) ))
)
); );
jest.mock('./ApplicationSchemaClassBase', () => ({ jest.mock('./ApplicationSchemaClassBase', () => ({
@ -233,9 +230,5 @@ describe('AppDetails component', () => {
expect(mockDeployApp).toHaveBeenCalled(); expect(mockDeployApp).toHaveBeenCalled();
expect(mockGetApplicationByName).toHaveBeenCalled(); expect(mockGetApplicationByName).toHaveBeenCalled();
userEvent.click(screen.getByRole('button', { name: 'Cancel AppSchedule' }));
expect(mockPush).toHaveBeenCalled();
}); });
}); });

View File

@ -24,9 +24,7 @@ import {
AppScheduleClass, AppScheduleClass,
AppType, AppType,
} from '../../../generated/entity/applications/app'; } from '../../../generated/entity/applications/app';
import { PipelineType } from '../../../generated/entity/services/ingestionPipelines/ingestionPipeline';
import { getIngestionPipelineByFqn } from '../../../rest/ingestionPipelineAPI'; import { getIngestionPipelineByFqn } from '../../../rest/ingestionPipelineAPI';
import { getIngestionFrequency } from '../../../utils/CommonUtils';
import TestSuiteScheduler from '../../AddDataQualityTest/components/TestSuiteScheduler'; import TestSuiteScheduler from '../../AddDataQualityTest/components/TestSuiteScheduler';
import Loader from '../../Loader/Loader'; import Loader from '../../Loader/Loader';
import AppRunsHistory from '../AppRunsHistory/AppRunsHistory.component'; import AppRunsHistory from '../AppRunsHistory/AppRunsHistory.component';
@ -74,10 +72,6 @@ const AppSchedule = ({
return cronstrue.toString(cronExp, { return cronstrue.toString(cronExp, {
throwExceptionOnParseError: false, throwExceptionOnParseError: false,
// Quartz cron format accepts 1-7 or SUN-SAT so need to increment index by 1
// Ref: https://www.quartz-scheduler.org/api/2.1.7/org/quartz/CronExpression.html
dayOfWeekStartIndexZero: false,
monthStartIndexZero: false,
}); });
} }
@ -225,13 +219,14 @@ const AppSchedule = ({
open={showModal} open={showModal}
title={t('label.update-entity', { entity: t('label.schedule') })}> title={t('label.update-entity', { entity: t('label.schedule') })}>
<TestSuiteScheduler <TestSuiteScheduler
isQuartzCron
buttonProps={{ buttonProps={{
cancelText: t('label.cancel'), cancelText: t('label.cancel'),
okText: t('label.save'), okText: t('label.save'),
}} }}
includePeriodOptions={initialOptions} includePeriodOptions={initialOptions}
initialData={getIngestionFrequency(PipelineType.Application)} initialData={
(appData.appSchedule as AppScheduleClass)?.cronExpression ?? ''
}
onCancel={onDialogCancel} onCancel={onDialogCancel}
onSubmit={onDialogSave} onSubmit={onDialogSave}
/> />

View File

@ -35,10 +35,6 @@ jest.mock('../../../rest/ingestionPipelineAPI', () => ({
.mockImplementation((...args) => mockGetIngestionPipelineByFqn(...args)), .mockImplementation((...args) => mockGetIngestionPipelineByFqn(...args)),
})); }));
jest.mock('../../../utils/CommonUtils', () => ({
getIngestionFrequency: jest.fn(),
}));
jest.mock('../../AddDataQualityTest/components/TestSuiteScheduler', () => jest.mock('../../AddDataQualityTest/components/TestSuiteScheduler', () =>
jest.fn().mockImplementation(({ onSubmit, onCancel }) => ( jest.fn().mockImplementation(({ onSubmit, onCancel }) => (
<div> <div>
@ -73,7 +69,6 @@ const mockProps1 = {
name: 'DataInsightsReportApplication', name: 'DataInsightsReportApplication',
}, },
onSave: mockOnSave, onSave: mockOnSave,
onCancel: jest.fn(),
onDemandTrigger: mockOnDemandTrigger, onDemandTrigger: mockOnDemandTrigger,
onDeployTrigger: mockOnDeployTrigger, onDeployTrigger: mockOnDeployTrigger,
}; };

View File

@ -14,7 +14,6 @@ import { App } from '../../../generated/entity/applications/app';
export interface AppScheduleProps { export interface AppScheduleProps {
appData: App; appData: App;
onCancel: () => void;
onSave: (cron: string) => void; onSave: (cron: string) => void;
onDemandTrigger: () => void; onDemandTrigger: () => void;
onDeployTrigger: () => void; onDeployTrigger: () => void;

View File

@ -125,10 +125,10 @@ const TeamHierarchy: FC<TeamHierarchyProps> = ({
render: (description: string) => ( render: (description: string) => (
<Typography.Paragraph <Typography.Paragraph
className="m-b-0" className="m-b-0"
style={{whiteSpace:"pre-wrap"}}
ellipsis={{ ellipsis={{
rows: 2, rows: 2,
}} }}
style={{ whiteSpace: 'pre-wrap' }}
title={description}> title={description}>
{isEmpty(description) ? '--' : description} {isEmpty(description) ? '--' : description}
</Typography.Paragraph> </Typography.Paragraph>

View File

@ -38,14 +38,11 @@ import {
ScheduleTimeline, ScheduleTimeline,
} from '../../generated/entity/applications/createAppRequest'; } from '../../generated/entity/applications/createAppRequest';
import { AppMarketPlaceDefinition } from '../../generated/entity/applications/marketplace/appMarketPlaceDefinition'; import { AppMarketPlaceDefinition } from '../../generated/entity/applications/marketplace/appMarketPlaceDefinition';
import { PipelineType } from '../../generated/entity/services/ingestionPipelines/ingestionPipeline';
import { useFqn } from '../../hooks/useFqn'; import { useFqn } from '../../hooks/useFqn';
import { installApplication } from '../../rest/applicationAPI'; import { installApplication } from '../../rest/applicationAPI';
import { getMarketPlaceApplicationByFqn } from '../../rest/applicationMarketPlaceAPI'; import { getMarketPlaceApplicationByFqn } from '../../rest/applicationMarketPlaceAPI';
import { import { getEntityMissingError } from '../../utils/CommonUtils';
getEntityMissingError, import { getCronInitialValue } from '../../utils/CronUtils';
getIngestionFrequency,
} from '../../utils/CommonUtils';
import { formatFormDataForSubmit } from '../../utils/JSONSchemaFormUtils'; import { formatFormDataForSubmit } from '../../utils/JSONSchemaFormUtils';
import { import {
getMarketPlaceAppDetailsPath, getMarketPlaceAppDetailsPath,
@ -72,6 +69,24 @@ const AppInstall = () => {
[appData] [appData]
); );
const { initialOptions, initialValue } = useMemo(() => {
let initialOptions;
if (appData?.name === 'DataInsightsReportApplication') {
initialOptions = ['Week'];
} else if (appData?.appType === AppType.External) {
initialOptions = ['Day'];
}
return {
initialOptions,
initialValue: getCronInitialValue(
appData?.appType ?? AppType.Internal,
appData?.name ?? ''
),
};
}, [appData?.name, appData?.appType]);
const fetchAppDetails = useCallback(async () => { const fetchAppDetails = useCallback(async () => {
setIsLoading(true); setIsLoading(true);
try { try {
@ -126,16 +141,6 @@ const AppInstall = () => {
setActiveServiceStep(3); setActiveServiceStep(3);
}; };
const initialOptions = useMemo(() => {
if (appData?.name === 'DataInsightsReportApplication') {
return ['Week'];
} else if (appData?.appType === AppType.External) {
return ['Day'];
}
return undefined;
}, [appData?.name, appData?.appType]);
const RenderSelectedTab = useCallback(() => { const RenderSelectedTab = useCallback(() => {
if (!appData || !jsonSchema) { if (!appData || !jsonSchema) {
return <></>; return <></>;
@ -180,9 +185,8 @@ const AppInstall = () => {
<div className="w-500 p-md border rounded-4"> <div className="w-500 p-md border rounded-4">
<Typography.Title level={5}>{t('label.schedule')}</Typography.Title> <Typography.Title level={5}>{t('label.schedule')}</Typography.Title>
<TestSuiteScheduler <TestSuiteScheduler
isQuartzCron
includePeriodOptions={initialOptions} includePeriodOptions={initialOptions}
initialData={getIngestionFrequency(PipelineType.Application)} initialData={initialValue}
onCancel={() => onCancel={() =>
setActiveServiceStep(appData.allowConfiguration ? 2 : 1) setActiveServiceStep(appData.allowConfiguration ? 2 : 1)
} }

View File

@ -11,7 +11,8 @@
* limitations under the License. * limitations under the License.
*/ */
import { getQuartzCronExpression } from './CronUtils'; import { AppType } from '../generated/entity/applications/app';
import { getCronInitialValue, getQuartzCronExpression } from './CronUtils';
describe('getQuartzCronExpression function', () => { describe('getQuartzCronExpression function', () => {
it('should generate cron expression for every minute', () => { it('should generate cron expression for every minute', () => {
@ -94,3 +95,32 @@ describe('getQuartzCronExpression function', () => {
expect(result).toEqual('0 30 10 ? * 4'); expect(result).toEqual('0 30 10 ? * 4');
}); });
}); });
describe('getCronInitialValue function', () => {
it('should generate hour cron expression if appType is internal and appName is not DataInsightsReportApplication', () => {
const result = getCronInitialValue(
AppType.Internal,
'SearchIndexingApplication'
);
expect(result).toEqual('0 * * * *');
});
it('should generate week cron expression if appName is DataInsightsReportApplication', () => {
const result = getCronInitialValue(
AppType.Internal,
'DataInsightsReportApplication'
);
expect(result).toEqual('0 0 * * 0');
});
it('should generate day cron expression if appType is external', () => {
const result = getCronInitialValue(
AppType.External,
'DataInsightsApplication'
);
expect(result).toEqual('0 0 * * *');
});
});

View File

@ -31,6 +31,7 @@ import {
StateValue, StateValue,
ToDisplay, ToDisplay,
} from '../components/common/CronEditor/CronEditor.interface'; } from '../components/common/CronEditor/CronEditor.interface';
import { AppType } from '../generated/entity/applications/app';
export const getQuartzCronExpression = (state: StateValue) => { export const getQuartzCronExpression = (state: StateValue) => {
const { const {
@ -172,3 +173,20 @@ export const getStateValue = (valueStr: string) => {
return stateVal; return stateVal;
}; };
export const getCronInitialValue = (appType: AppType, appName: string) => {
const value = {
min: 0,
hour: 0,
};
let initialValue = getHourCron(value);
if (appName === 'DataInsightsReportApplication') {
initialValue = getWeekCron({ ...value, dow: 0 });
} else if (appType === AppType.External) {
initialValue = getDayCron(value);
}
return initialValue;
};