fix: #19905 Data quality pipeline page is only displaying a maximum of 10 pipelines (#20266)

This commit is contained in:
Shailesh Parmar 2025-03-16 10:21:26 +05:30 committed by GitHub
parent d0611388f4
commit 4dc4053dbb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 284 additions and 25 deletions

View File

@ -19,6 +19,7 @@ import QueryString from 'qs';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useHistory } from 'react-router-dom';
import { PAGE_SIZE_BASE } from '../../../../constants/constants';
import { usePermissionProvider } from '../../../../context/PermissionProvider/PermissionProvider';
import { ResourceEntity } from '../../../../context/PermissionProvider/PermissionProvider.interface';
import { ERROR_PLACEHOLDER_TYPE } from '../../../../enums/common.enum';
@ -29,6 +30,8 @@ import { Table as TableType } from '../../../../generated/entity/data/table';
import { Operation } from '../../../../generated/entity/policies/policy';
import { IngestionPipeline } from '../../../../generated/entity/services/ingestionPipelines/ingestionPipeline';
import { TestSuite } from '../../../../generated/tests/testCase';
import { Paging } from '../../../../generated/type/paging';
import { usePaging } from '../../../../hooks/paging/usePaging';
import { useAirflowStatus } from '../../../../hooks/useAirflowStatus';
import {
deployIngestionPipelineById,
@ -42,6 +45,7 @@ import { getServiceFromTestSuiteFQN } from '../../../../utils/TestSuiteUtils';
import { showErrorToast, showSuccessToast } from '../../../../utils/ToastUtils';
import ErrorPlaceHolder from '../../../common/ErrorWithPlaceholder/ErrorPlaceHolder';
import ErrorPlaceHolderIngestion from '../../../common/ErrorWithPlaceholder/ErrorPlaceHolderIngestion';
import { PagingHandlerParams } from '../../../common/NextPrevious/NextPrevious.interface';
import IngestionListTable from '../../../Settings/Services/Ingestion/IngestionListTable/IngestionListTable';
interface Props {
@ -58,6 +62,8 @@ const TestSuitePipelineTab = ({
const testSuiteFQN = testSuite?.fullyQualifiedName ?? testSuite?.name ?? '';
const { permissions } = usePermissionProvider();
const pipelinePaging = usePaging(PAGE_SIZE_BASE);
const { pageSize, handlePagingChange } = pipelinePaging;
const history = useHistory();
const [isLoading, setIsLoading] = useState(true);
@ -89,24 +95,44 @@ const TestSuitePipelineTab = ({
[permissions]
);
const getAllIngestionWorkflows = useCallback(async () => {
try {
setIsLoading(true);
const response = await getIngestionPipelines({
arrQueryFields: [
TabSpecificField.OWNERS,
TabSpecificField.PIPELINE_STATUSES,
],
testSuite: testSuiteFQN,
pipelineType: [PipelineType.TestSuite],
});
setTestSuitePipelines(response.data);
} catch (error) {
showErrorToast(error as AxiosError);
} finally {
setIsLoading(false);
}
}, [testSuiteFQN]);
const getAllIngestionWorkflows = useCallback(
async (paging?: Omit<Paging, 'total'>, limit?: number) => {
try {
setIsLoading(true);
const response = await getIngestionPipelines({
arrQueryFields: [
TabSpecificField.OWNERS,
TabSpecificField.PIPELINE_STATUSES,
],
testSuite: testSuiteFQN,
pipelineType: [PipelineType.TestSuite],
paging,
limit: limit ?? pageSize,
});
setTestSuitePipelines(response.data);
handlePagingChange(response.paging);
} catch (error) {
showErrorToast(error as AxiosError);
} finally {
setIsLoading(false);
}
},
[testSuiteFQN, pageSize, handlePagingChange]
);
const handlePipelinePageChange = useCallback(
({ cursorType, currentPage }: PagingHandlerParams) => {
const { paging, handlePageChange } = pipelinePaging;
if (cursorType) {
getAllIngestionWorkflows(
{ [cursorType]: paging[cursorType] },
pageSize
);
handlePageChange(currentPage);
}
},
[getAllIngestionWorkflows, pipelinePaging]
);
const handleAddPipelineRedirection = () => {
history.push({
@ -195,8 +221,8 @@ const TestSuitePipelineTab = ({
}, [testSuitePipelines]);
useEffect(() => {
getAllIngestionWorkflows();
}, []);
getAllIngestionWorkflows(undefined, pageSize);
}, [pageSize]);
const emptyPlaceholder = useMemo(
() =>
@ -252,6 +278,7 @@ const TestSuitePipelineTab = ({
handleIngestionListUpdate={handlePipelineListUpdate}
handlePipelineIdToFetchStatus={handlePipelineIdToFetchStatus}
ingestionData={testSuitePipelines}
ingestionPagingInfo={pipelinePaging}
isLoading={isLoading}
pipelineIdToFetchStatus={pipelineIdToFetchStatus}
serviceCategory={ServiceCategory.DATABASE_SERVICES}
@ -259,6 +286,7 @@ const TestSuitePipelineTab = ({
tableClassName="test-suite-pipeline-tab"
triggerIngestion={handleTriggerIngestion}
onIngestionWorkflowsUpdate={getAllIngestionWorkflows}
onPageChange={handlePipelinePageChange}
/>
</Col>
</Row>

View File

@ -10,10 +10,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { render } from '@testing-library/react';
import { render, screen } from '@testing-library/react';
import React from 'react';
import { act } from 'react-test-renderer';
import { PAGE_SIZE_BASE } from '../../../../constants/constants';
import { Table } from '../../../../generated/entity/data/table';
import { IngestionPipeline } from '../../../../generated/entity/services/ingestionPipelines/ingestionPipeline';
import { useAirflowStatus } from '../../../../hooks/useAirflowStatus';
import { getIngestionPipelines } from '../../../../rest/ingestionPipelineAPI';
import TestSuitePipelineTab from './TestSuitePipelineTab.component';
@ -55,15 +58,124 @@ const mockTestSuite = {
testCaseResultSummary: [],
} as unknown as Table['testSuite'];
const mockPipelines = [
{
id: '1',
name: 'pipeline1',
sourceConfig: {
config: {
testCases: ['test1', 'test2', 'test3'],
},
},
},
{
id: '2',
name: 'pipeline2',
sourceConfig: {
config: {
testCases: ['test1'],
},
},
},
{
id: '3',
name: 'pipeline3',
sourceConfig: {
config: {},
},
},
];
const mockPaging = {
after: 'after-id',
before: 'before-id',
total: 10,
};
jest.mock('../../../../rest/ingestionPipelineAPI', () => {
return {
getIngestionPipelines: jest
.fn()
.mockImplementation(() => Promise.resolve()),
.mockImplementation(() =>
Promise.resolve({ data: mockPipelines, paging: mockPaging })
),
};
});
jest.mock('../../../../hooks/useAirflowStatus', () => ({
useAirflowStatus: jest.fn().mockReturnValue({
isAirflowAvailable: true,
isFetchingStatus: false,
}),
}));
jest.mock('react-router-dom', () => ({
useHistory: jest.fn(),
useLocation: jest.fn().mockReturnValue({
pathname: '/test/path',
search: '',
hash: '',
state: null,
}),
}));
jest.mock('../../../../context/PermissionProvider/PermissionProvider', () => ({
usePermissionProvider: jest.fn().mockReturnValue({
permissions: {},
}),
}));
jest.mock(
'../../../Settings/Services/Ingestion/IngestionListTable/IngestionListTable',
() => {
return function MockIngestionListTable({
ingestionData,
onPageChange,
}: {
ingestionData: IngestionPipeline[];
onPageChange: ({
cursorType,
currentPage,
}: {
cursorType: string;
currentPage: number;
}) => void;
}) {
return (
<div data-testid="test-suite-pipeline-tab">
{ingestionData.map((pipeline) => (
<div key={pipeline.id}>{pipeline.name}</div>
))}
<button
onClick={() =>
onPageChange({ cursorType: 'after', currentPage: 2 })
}>
Next Page
</button>
</div>
);
};
}
);
jest.mock(
'../../../common/ErrorWithPlaceholder/ErrorPlaceHolderIngestion',
() => {
return jest
.fn()
.mockImplementation(() => (
<div data-testid="error-placeholder-ingestion">
Airflow not available
</div>
));
}
);
describe('TestSuite Pipeline component', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('getIngestionPipelines API should call on page load', async () => {
const mockGetIngestionPipelines = getIngestionPipelines as jest.Mock;
await act(async () => {
@ -74,6 +186,107 @@ describe('TestSuite Pipeline component', () => {
arrQueryFields: ['owners', 'pipelineStatuses'],
pipelineType: ['TestSuite'],
testSuite: mockTestSuite?.fullyQualifiedName,
limit: PAGE_SIZE_BASE,
});
});
it('should call getIngestionPipelines with correct paging parameters', async () => {
const mockGetIngestionPipelines = getIngestionPipelines as jest.Mock;
await act(async () => {
render(<TestSuitePipelineTab testSuite={mockTestSuite} />);
});
// Initial call with default page size
expect(mockGetIngestionPipelines).toHaveBeenCalledWith(
expect.objectContaining({
limit: PAGE_SIZE_BASE,
})
);
// Click next page button
const nextPageButton = screen.getByText('Next Page');
await act(async () => {
nextPageButton.click();
});
// Verify call with after cursor
expect(mockGetIngestionPipelines).toHaveBeenCalledWith(
expect.objectContaining({
paging: { after: 'after-id' },
limit: PAGE_SIZE_BASE,
})
);
});
it('should update pipeline list when page changes', async () => {
const updatedMockPipelines = [
{
id: '4',
name: 'pipeline4',
sourceConfig: { config: {} },
},
];
const mockGetIngestionPipelines = getIngestionPipelines as jest.Mock;
mockGetIngestionPipelines
.mockImplementationOnce(() =>
Promise.resolve({ data: mockPipelines, paging: mockPaging })
)
.mockImplementationOnce(() =>
Promise.resolve({
data: updatedMockPipelines,
paging: { ...mockPaging, after: null },
})
);
await act(async () => {
render(<TestSuitePipelineTab testSuite={mockTestSuite} />);
});
// Initial render should show first page pipelines
expect(screen.getByText('pipeline1')).toBeInTheDocument();
// Click next page button
const nextPageButton = screen.getByText('Next Page');
await act(async () => {
nextPageButton.click();
});
// Should show second page pipeline
expect(screen.getByText('pipeline4')).toBeInTheDocument();
});
it('should show error placeholder when airflow is not available', async () => {
// Mock airflow status as unavailable
(useAirflowStatus as jest.Mock).mockReturnValue({
isAirflowAvailable: false,
isFetchingStatus: false,
});
await act(async () => {
render(<TestSuitePipelineTab testSuite={mockTestSuite} />);
});
expect(
screen.getByTestId('error-placeholder-ingestion')
).toBeInTheDocument();
});
it('should show loading state while fetching airflow status', async () => {
// Mock airflow status as loading
(useAirflowStatus as jest.Mock).mockReturnValue({
isAirflowAvailable: true,
isFetchingStatus: true,
});
await act(async () => {
render(<TestSuitePipelineTab testSuite={mockTestSuite} />);
});
// Component should be in loading state
expect(screen.queryByTestId('error-placeholder')).not.toBeInTheDocument();
expect(
screen.queryByTestId('error-placeholder-ingestion')
).not.toBeInTheDocument();
});
});

View File

@ -43,6 +43,7 @@ import { EntityTabs, EntityType } from '../../../../../enums/entity.enum';
import { ProfilerDashboardType } from '../../../../../enums/table.enum';
import { TestCaseStatus } from '../../../../../generated/tests/testCase';
import LimitWrapper from '../../../../../hoc/LimitWrapper';
import useCustomLocation from '../../../../../hooks/useCustomLocation/useCustomLocation';
import { useFqn } from '../../../../../hooks/useFqn';
import {
ListTestCaseParamsBySearch,
@ -96,6 +97,7 @@ export const QualityTab = () => {
}, [permissions]);
const { fqn: datasetFQN } = useFqn();
const history = useHistory();
const location = useCustomLocation();
const { t } = useTranslation();
const [selectedTestCaseStatus, setSelectedTestCaseStatus] =
@ -249,6 +251,14 @@ export const QualityTab = () => {
history.push(getAddDataQualityTableTestPath(type, datasetFQN));
};
const handleTabChange = () => {
history.replace({
pathname: location.pathname,
search: location.search,
state: undefined,
});
};
const addButtonContent = useMemo(
() => [
{
@ -333,7 +343,7 @@ export const QualityTab = () => {
<SummaryPanel testSummary={testCaseSummary ?? INITIAL_TEST_SUMMARY} />
</Col>
<Col span={24}>
<Tabs items={tabs} />
<Tabs items={tabs} onChange={handleTabChange} />
</Col>
</Row>
);

View File

@ -121,6 +121,14 @@ jest.mock('../../../../../hoc/LimitWrapper', () => {
));
});
jest.mock('../../../../../hooks/useCustomLocation/useCustomLocation', () => ({
__esModule: true,
default: jest.fn().mockImplementation(() => ({
pathname: '/test-path',
search: '?test=value',
})),
}));
describe('QualityTab', () => {
it('should render QualityTab', async () => {
await act(async () => {

View File

@ -24,7 +24,7 @@ import React, {
useState,
} from 'react';
import { useTranslation } from 'react-i18next';
import { PAGE_SIZE } from '../../../../constants/constants';
import { PAGE_SIZE_BASE } from '../../../../constants/constants';
import { mockDatasetData } from '../../../../constants/mockTourData.constants';
import {
DEFAULT_RANGE_DATA,
@ -71,7 +71,7 @@ export const TableProfilerProvider = ({
const { t } = useTranslation();
const { fqn: datasetFQN } = useFqn();
const { isTourOpen } = useTourProvider();
const testCasePaging = usePaging(PAGE_SIZE);
const testCasePaging = usePaging(PAGE_SIZE_BASE);
const location = useCustomLocation();
// profiler has its own api but sent's the data in Table type
const [tableProfiler, setTableProfiler] = useState<Table>();