mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-09-25 17:04:54 +00:00
Enhance Permission Handling in Data Quality Components
- Integrated permission checks for creating and editing test cases in TestCaseFormV1 and BundleSuiteForm. - Updated state management to reflect permission-based conditions for enabling features like pipeline creation and scheduler options. - Refactored related components to ensure proper dependency handling in hooks. - Improved test coverage for permission-related functionalities across affected components.
This commit is contained in:
parent
af39ce3515
commit
1373b0456a
@ -137,6 +137,25 @@ jest.mock('../../../../context/LimitsProvider/useLimitsStore', () => ({
|
||||
useLimitStore: () => ({ getResourceLimit: () => 100 }),
|
||||
}));
|
||||
|
||||
jest.mock('../../../../context/PermissionProvider/PermissionProvider', () => ({
|
||||
usePermissionProvider: () => ({
|
||||
getEntityPermissionByFqn: jest.fn().mockResolvedValue({
|
||||
EditAll: true,
|
||||
EditTests: true,
|
||||
}),
|
||||
permissions: {
|
||||
ingestionPipeline: {
|
||||
Create: true,
|
||||
EditAll: true,
|
||||
},
|
||||
testCase: {
|
||||
Create: true,
|
||||
EditAll: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('crypto-random-string-with-promisify-polyfill', () =>
|
||||
jest.fn().mockReturnValue('ABC123')
|
||||
);
|
||||
|
@ -147,7 +147,7 @@ const TestCaseFormV1: FC<TestCaseFormV1Props> = ({
|
||||
const [form] = useForm<FormValues>();
|
||||
const { isAirflowAvailable } = useAirflowStatus();
|
||||
const { getEntityPermissionByFqn, permissions } = usePermissionProvider();
|
||||
const { ingestionPipeline } = permissions;
|
||||
const { ingestionPipeline, testCase } = permissions;
|
||||
|
||||
const TEST_LEVEL_OPTIONS: SelectionOption[] = [
|
||||
{
|
||||
@ -457,19 +457,22 @@ const TestCaseFormV1: FC<TestCaseFormV1Props> = ({
|
||||
// HOOKS - Callbacks (grouped by functionality)
|
||||
// =============================================
|
||||
// Pipeline-related callbacks
|
||||
const checkExistingPipelines = useCallback(async (testSuiteFqn: string) => {
|
||||
try {
|
||||
const { paging } = await getIngestionPipelines({
|
||||
testSuite: testSuiteFqn,
|
||||
pipelineType: [PipelineType.TestSuite],
|
||||
arrQueryFields: ['id'],
|
||||
limit: 0,
|
||||
});
|
||||
setCanCreatePipeline(paging.total === 0);
|
||||
} catch (error) {
|
||||
setCanCreatePipeline(true);
|
||||
}
|
||||
}, []);
|
||||
const checkExistingPipelines = useCallback(
|
||||
async (testSuiteFqn: string) => {
|
||||
try {
|
||||
const { paging } = await getIngestionPipelines({
|
||||
testSuite: testSuiteFqn,
|
||||
pipelineType: [PipelineType.TestSuite],
|
||||
arrQueryFields: ['id'],
|
||||
limit: 0,
|
||||
});
|
||||
setCanCreatePipeline(paging.total === 0);
|
||||
} catch (error) {
|
||||
setCanCreatePipeline(true);
|
||||
}
|
||||
},
|
||||
[ingestionPipeline]
|
||||
);
|
||||
|
||||
// Form interaction callbacks
|
||||
const handleCancel = useCallback(() => {
|
||||
@ -647,8 +650,11 @@ const TestCaseFormV1: FC<TestCaseFormV1Props> = ({
|
||||
// Permission checking callback
|
||||
const checkTablePermissions = useCallback(
|
||||
async (tableFqn: string) => {
|
||||
setIsCheckingPermissions(true);
|
||||
if (testCase.Create) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
setIsCheckingPermissions(true);
|
||||
try {
|
||||
const permissions = await getEntityPermissionByFqn(
|
||||
ResourceEntity.TABLE,
|
||||
@ -656,6 +662,8 @@ const TestCaseFormV1: FC<TestCaseFormV1Props> = ({
|
||||
);
|
||||
const canCreate = permissions.EditAll || permissions.EditTests;
|
||||
|
||||
setCanCreatePipeline(canCreate);
|
||||
|
||||
if (!canCreate) {
|
||||
// Return false to trigger validation error
|
||||
return Promise.reject(
|
||||
@ -884,24 +892,31 @@ const TestCaseFormV1: FC<TestCaseFormV1Props> = ({
|
||||
|
||||
// Check for existing pipelines when table is selected
|
||||
useEffect(() => {
|
||||
if (selectedTableData?.testSuite?.fullyQualifiedName) {
|
||||
// Table has test suite - check for existing pipelines
|
||||
checkExistingPipelines(selectedTableData.testSuite.fullyQualifiedName);
|
||||
} else if (testSuite?.fullyQualifiedName) {
|
||||
// Using provided test suite - check for existing pipelines
|
||||
checkExistingPipelines(testSuite.fullyQualifiedName);
|
||||
} else if (selectedTable) {
|
||||
// Table selected but no test suite - can create pipeline (test suite will be created)
|
||||
setCanCreatePipeline(true);
|
||||
} else {
|
||||
// No table selected - hide scheduler
|
||||
// Early return if user doesn't have permission to create pipelines or Early return if no table is selected
|
||||
if (!ingestionPipeline.Create || !selectedTable) {
|
||||
setCanCreatePipeline(false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the test suite FQN from either the table data or provided test suite
|
||||
const testSuiteFqn =
|
||||
selectedTableData?.testSuite?.fullyQualifiedName ||
|
||||
testSuite?.fullyQualifiedName;
|
||||
|
||||
if (testSuiteFqn) {
|
||||
// Check for existing pipelines if we have a test suite
|
||||
checkExistingPipelines(testSuiteFqn);
|
||||
} else {
|
||||
// No test suite exists - can create pipeline (test suite will be created)
|
||||
setCanCreatePipeline(true);
|
||||
}
|
||||
}, [
|
||||
selectedTableData?.testSuite?.fullyQualifiedName,
|
||||
testSuite?.fullyQualifiedName,
|
||||
selectedTable,
|
||||
checkExistingPipelines,
|
||||
ingestionPipeline,
|
||||
]);
|
||||
|
||||
// Initialize manual edit flag
|
||||
|
@ -100,7 +100,7 @@ export const AddTestCaseList = ({
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
[selectedTest, filters, testCaseParams]
|
||||
[selectedTest]
|
||||
);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
@ -123,7 +123,7 @@ export const AddTestCaseList = ({
|
||||
});
|
||||
}
|
||||
},
|
||||
[searchTerm, totalCount, items, isLoading, pageNumber, fetchTestCases]
|
||||
[searchTerm, totalCount, items, isLoading]
|
||||
);
|
||||
|
||||
const handleCardClick = (details: TestCase) => {
|
||||
@ -163,7 +163,7 @@ export const AddTestCaseList = ({
|
||||
};
|
||||
useEffect(() => {
|
||||
fetchTestCases({ searchText: searchTerm });
|
||||
}, [searchTerm, fetchTestCases]);
|
||||
}, [searchTerm]);
|
||||
|
||||
const renderList = useMemo(() => {
|
||||
if (!isLoading && isEmpty(items)) {
|
||||
|
@ -97,6 +97,17 @@ jest.mock('../../../context/LimitsProvider/useLimitsStore', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('../../../context/PermissionProvider/PermissionProvider', () => ({
|
||||
usePermissionProvider: () => ({
|
||||
permissions: {
|
||||
ingestionPipeline: {
|
||||
Create: true,
|
||||
EditAll: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('../../../hooks/useApplicationStore', () => ({
|
||||
useApplicationStore: jest.fn().mockReturnValue({
|
||||
currentUser: {
|
||||
@ -208,7 +219,9 @@ describe('BundleSuiteForm Component', () => {
|
||||
it('should render form in drawer mode', async () => {
|
||||
render(<BundleSuiteForm {...mockProps} />);
|
||||
|
||||
expect(await screen.findAllByText('label.create-entity')).toHaveLength(2); // One in header, one in card title
|
||||
expect(
|
||||
await screen.findByText('label.create-entity')
|
||||
).toBeInTheDocument(); // Should appear in card title
|
||||
expect(document.querySelector('.bundle-suite-form')).toBeInTheDocument();
|
||||
expect(document.querySelector('.drawer-mode')).toBeInTheDocument();
|
||||
});
|
||||
|
@ -34,6 +34,7 @@ import { MAX_NAME_LENGTH } from '../../../constants/constants';
|
||||
import { DEFAULT_SCHEDULE_CRON_DAILY } from '../../../constants/Schedular.constants';
|
||||
import { useAirflowStatus } from '../../../context/AirflowStatusProvider/AirflowStatusProvider';
|
||||
import { useLimitStore } from '../../../context/LimitsProvider/useLimitsStore';
|
||||
import { usePermissionProvider } from '../../../context/PermissionProvider/PermissionProvider';
|
||||
import { OwnerType } from '../../../enums/user.enum';
|
||||
import {
|
||||
ConfigType,
|
||||
@ -92,6 +93,8 @@ const BundleSuiteForm: React.FC<BundleSuiteFormProps> = ({
|
||||
const { config } = useLimitStore();
|
||||
const { currentUser } = useApplicationStore();
|
||||
const { isAirflowAvailable } = useAirflowStatus();
|
||||
const { permissions } = usePermissionProvider();
|
||||
const { ingestionPipeline } = permissions;
|
||||
const enableScheduler = Form.useWatch('enableScheduler', form);
|
||||
|
||||
// =============================================
|
||||
@ -266,7 +269,7 @@ const BundleSuiteForm: React.FC<BundleSuiteFormProps> = ({
|
||||
testSuiteId: testSuite.id ?? '',
|
||||
});
|
||||
|
||||
if (formData.enableScheduler) {
|
||||
if (formData.enableScheduler && ingestionPipeline.Create) {
|
||||
await createAndDeployPipeline(testSuite, formData);
|
||||
}
|
||||
|
||||
@ -391,75 +394,77 @@ const BundleSuiteForm: React.FC<BundleSuiteFormProps> = ({
|
||||
</Card>
|
||||
|
||||
{/* Scheduler with Toggle */}
|
||||
<Card className="form-card-section" data-testid="scheduler-card">
|
||||
<div className="card-title-container d-flex items-center gap-3 m-b-lg">
|
||||
<Form.Item
|
||||
noStyle
|
||||
className="m-b-0"
|
||||
name="enableScheduler"
|
||||
valuePropName="checked">
|
||||
<Switch data-testid="scheduler-toggle" />
|
||||
</Form.Item>
|
||||
<div>
|
||||
<Typography.Paragraph className="card-title-text m-0">
|
||||
{t('label.create-entity', {
|
||||
entity: t('label.pipeline'),
|
||||
})}
|
||||
</Typography.Paragraph>
|
||||
<Typography.Paragraph className="card-title-description m-0">
|
||||
{`${t('message.pipeline-entity-description', {
|
||||
entity: t('label.bundle-suite'),
|
||||
})} (${t('label.optional')})`}
|
||||
</Typography.Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Scheduler form fields - only show when toggle is enabled */}
|
||||
{enableScheduler && (
|
||||
<>
|
||||
{generateFormFields(schedulerFormFields)}
|
||||
|
||||
<Form.Item label={t('label.schedule-interval')} name="cron">
|
||||
<ScheduleIntervalV1
|
||||
entity={t('label.test-suite')}
|
||||
includePeriodOptions={schedulerOptions}
|
||||
/>
|
||||
{ingestionPipeline.Create && (
|
||||
<Card className="form-card-section" data-testid="scheduler-card">
|
||||
<div className="card-title-container d-flex items-center gap-3 m-b-lg">
|
||||
<Form.Item
|
||||
noStyle
|
||||
className="m-b-0"
|
||||
name="enableScheduler"
|
||||
valuePropName="checked">
|
||||
<Switch data-testid="scheduler-toggle" />
|
||||
</Form.Item>
|
||||
|
||||
{/* Debug Log and Raise on Error switches */}
|
||||
<div className="m-t-md">
|
||||
<Row gutter={[24, 16]}>
|
||||
<Col span={12}>
|
||||
<div className="d-flex gap-2 form-switch-container">
|
||||
<Form.Item
|
||||
className="m-b-0"
|
||||
name="enableDebugLog"
|
||||
valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Typography.Paragraph className="font-medium m-0">
|
||||
{t('label.enable-debug-log')}
|
||||
</Typography.Paragraph>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<div className="d-flex gap-2 form-switch-container">
|
||||
<Form.Item
|
||||
className="m-b-0"
|
||||
name="raiseOnError"
|
||||
valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Typography.Paragraph className="font-medium m-0">
|
||||
{t('label.raise-on-error')}
|
||||
</Typography.Paragraph>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
<div>
|
||||
<Typography.Paragraph className="card-title-text m-0">
|
||||
{t('label.create-entity', {
|
||||
entity: t('label.pipeline'),
|
||||
})}
|
||||
</Typography.Paragraph>
|
||||
<Typography.Paragraph className="card-title-description m-0">
|
||||
{`${t('message.pipeline-entity-description', {
|
||||
entity: t('label.bundle-suite'),
|
||||
})} (${t('label.optional')})`}
|
||||
</Typography.Paragraph>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Scheduler form fields - only show when toggle is enabled */}
|
||||
{enableScheduler && (
|
||||
<>
|
||||
{generateFormFields(schedulerFormFields)}
|
||||
|
||||
<Form.Item label={t('label.schedule-interval')} name="cron">
|
||||
<ScheduleIntervalV1
|
||||
entity={t('label.test-suite')}
|
||||
includePeriodOptions={schedulerOptions}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{/* Debug Log and Raise on Error switches */}
|
||||
<div className="m-t-md">
|
||||
<Row gutter={[24, 16]}>
|
||||
<Col span={12}>
|
||||
<div className="d-flex gap-2 form-switch-container">
|
||||
<Form.Item
|
||||
className="m-b-0"
|
||||
name="enableDebugLog"
|
||||
valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Typography.Paragraph className="font-medium m-0">
|
||||
{t('label.enable-debug-log')}
|
||||
</Typography.Paragraph>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<div className="d-flex gap-2 form-switch-container">
|
||||
<Form.Item
|
||||
className="m-b-0"
|
||||
name="raiseOnError"
|
||||
valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Typography.Paragraph className="font-medium m-0">
|
||||
{t('label.raise-on-error')}
|
||||
</Typography.Paragraph>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
)}
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
|
Loading…
x
Reference in New Issue
Block a user