mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-11-13 17:32:53 +00:00
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:
parent
8bea265f0a
commit
847b2d8bc9
@ -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');
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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}>
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 * * *');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@ -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;
|
||||||
|
};
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user