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:
Shailesh Parmar 2025-07-13 18:36:27 +05:30
parent af39ce3515
commit 1373b0456a
5 changed files with 150 additions and 98 deletions

View File

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

View File

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

View File

@ -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)) {

View File

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

View File

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