GEN-1296: sort test cases by latest run (#18097)

* GEN-1296: sort test cases by latest run

* minor fix

* added sort support for name field

* fixed unit test

* fixed failing playwright test

* fixed failing playwright test
This commit is contained in:
Shailesh Parmar 2024-10-07 12:34:09 +05:30 committed by GitHub
parent 49fceb4674
commit a74811481f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 209 additions and 39 deletions

View File

@ -79,4 +79,7 @@ Thumbs.db
src/generated
# Snapshots
*.snap
*.snap
# env
".env"

View File

@ -231,7 +231,7 @@ test.describe('Incident Manager', PLAYWRIGHT_INGESTION_TAG_OBJ, () => {
await test.step('Resolve task from incident list page', async () => {
await visitProfilerTab(page, table1);
const testCaseResponse = page.waitForResponse(
'/api/v1/dataQuality/testCases/search/list?fields=*'
'/api/v1/dataQuality/testCases/search/list?*fields=*'
);
await page
.getByTestId('profiler-tab-left-panel')
@ -277,7 +277,7 @@ test.describe('Incident Manager', PLAYWRIGHT_INGESTION_TAG_OBJ, () => {
await test.step('Task should be closed', async () => {
await visitProfilerTab(page, table1);
const testCaseResponse = page.waitForResponse(
'/api/v1/dataQuality/testCases/search/list?fields=*'
'/api/v1/dataQuality/testCases/search/list?*fields=*'
);
await page
.getByTestId('profiler-tab-left-panel')
@ -373,7 +373,7 @@ test.describe('Incident Manager', PLAYWRIGHT_INGESTION_TAG_OBJ, () => {
await test.step("Verify incident's status on DQ page", async () => {
await visitProfilerTab(page, table1);
const testCaseResponse = page.waitForResponse(
'/api/v1/dataQuality/testCases/search/list?fields=*'
'/api/v1/dataQuality/testCases/search/list?*fields=*'
);
await page
.getByTestId('profiler-tab-left-panel')

View File

@ -130,7 +130,7 @@ test('Table test case', PLAYWRIGHT_INGESTION_TAG_OBJ, async ({ page }) => {
).toBeVisible();
const testCaseResponse = page.waitForResponse(
'/api/v1/dataQuality/testCases/search/list?fields=*'
'/api/v1/dataQuality/testCases/search/list?*fields=*'
);
await page.click(`[data-testid="view-service-button"]`);
await testCaseResponse;
@ -210,7 +210,7 @@ test('Column test case', PLAYWRIGHT_INGESTION_TAG_OBJ, async ({ page }) => {
await page.waitForSelector('[data-testid="view-service-button"]');
const testCaseResponse = page.waitForResponse(
'/api/v1/dataQuality/testCases/search/list?fields=*'
'/api/v1/dataQuality/testCases/search/list?*fields=*'
);
await page.click(`[data-testid="view-service-button"]`);
await testCaseResponse;

View File

@ -99,7 +99,7 @@ test('Table difference test case', async ({ page }) => {
await page.getByTestId('submit-test').click();
await createTestCaseResponse;
const tableTestResponse = page.waitForResponse(
`/api/v1/dataQuality/testCases/search/list?fields=*`
`/api/v1/dataQuality/testCases/search/list?*fields=*`
);
await page.getByTestId('view-service-button').click();
await tableTestResponse;
@ -196,7 +196,7 @@ test('Custom SQL Query', async ({ page }) => {
await page.getByTestId('submit-test').click();
await createTestCaseResponse;
const tableTestResponse = page.waitForResponse(
`/api/v1/dataQuality/testCases/search/list?fields=*`
`/api/v1/dataQuality/testCases/search/list?*fields=*`
);
await page.getByTestId('view-service-button').click();
await tableTestResponse;
@ -300,7 +300,7 @@ test('Column Values To Be Not Null', async ({ page }) => {
await page.waitForSelector('[data-testid="success-line"]');
await page.waitForSelector('[data-testid="view-service-button"]');
const testCaseResponse = page.waitForResponse(
'/api/v1/dataQuality/testCases/search/list?fields=*'
'/api/v1/dataQuality/testCases/search/list?*fields=*'
);
await page.click(`[data-testid="view-service-button"]`);
await testCaseResponse;

View File

@ -32,7 +32,7 @@ export const visitDataQualityTab = async (page: Page, table: TableClass) => {
await table.visitEntityPage(page);
await page.getByTestId('profiler').click();
const testCaseResponse = page.waitForResponse(
'/api/v1/dataQuality/testCases/search/list?fields=*'
'/api/v1/dataQuality/testCases/search/list?*fields=*'
);
await page
.getByTestId('profiler-tab-left-panel')

View File

@ -53,13 +53,14 @@ import {
TIER_CATEGORY,
} from '../../../constants/constants';
import {
DEFAULT_SORT_ORDER,
TEST_CASE_FILTERS,
TEST_CASE_PLATFORM_OPTION,
TEST_CASE_STATUS_OPTION,
TEST_CASE_TYPE_OPTION,
} from '../../../constants/profiler.constant';
import { usePermissionProvider } from '../../../context/PermissionProvider/PermissionProvider';
import { ERROR_PLACEHOLDER_TYPE, SORT_ORDER } from '../../../enums/common.enum';
import { ERROR_PLACEHOLDER_TYPE } from '../../../enums/common.enum';
import { TabSpecificField } from '../../../enums/entity.enum';
import { SearchIndex } from '../../../enums/search.enum';
import { TestCase } from '../../../generated/tests/testCase';
@ -68,7 +69,10 @@ import useCustomLocation from '../../../hooks/useCustomLocation/useCustomLocatio
import { DataQualityPageTabs } from '../../../pages/DataQuality/DataQualityPage.interface';
import { searchQuery } from '../../../rest/searchAPI';
import { getTags } from '../../../rest/tagAPI';
import { getListTestCaseBySearch } from '../../../rest/testAPI';
import {
getListTestCaseBySearch,
ListTestCaseParamsBySearch,
} from '../../../rest/testAPI';
import { getTestCaseFiltersValue } from '../../../utils/DataQuality/DataQualityUtils';
import { getEntityName } from '../../../utils/EntityUtils';
import { getDataQualityPagePath } from '../../../utils/RouterUtils';
@ -94,6 +98,8 @@ export const TestCases = ({ summaryPanel }: { summaryPanel: ReactNode }) => {
const [tagOptions, setTagOptions] = useState<DefaultOptionType[]>([]);
const [tierOptions, setTierOptions] = useState<DefaultOptionType[]>([]);
const [serviceOptions, setServiceOptions] = useState<DefaultOptionType[]>([]);
const [sortOptions, setSortOptions] =
useState<ListTestCaseParamsBySearch>(DEFAULT_SORT_ORDER);
const params = useMemo(() => {
const search = location.search;
@ -146,7 +152,8 @@ export const TestCases = ({ summaryPanel }: { summaryPanel: ReactNode }) => {
const fetchTestCases = async (
currentPage = INITIAL_PAGING_VALUE,
filters?: string[]
filters?: string[],
apiParams?: ListTestCaseParamsBySearch
) => {
const updatedParams = getTestCaseFiltersValue(
params,
@ -157,6 +164,8 @@ export const TestCases = ({ summaryPanel }: { summaryPanel: ReactNode }) => {
try {
const { data, paging } = await getListTestCaseBySearch({
...updatedParams,
...sortOptions,
...apiParams,
testCaseStatus: isEmpty(params?.testCaseStatus)
? undefined
: params?.testCaseStatus,
@ -169,8 +178,6 @@ export const TestCases = ({ summaryPanel }: { summaryPanel: ReactNode }) => {
],
q: searchValue ? `*${searchValue}*` : undefined,
offset: (currentPage - 1) * pageSize,
sortType: SORT_ORDER.DESC,
sortField: 'testCaseResult.timestamp',
});
setTestCase(data);
handlePagingChange(paging);
@ -182,6 +189,16 @@ export const TestCases = ({ summaryPanel }: { summaryPanel: ReactNode }) => {
}
};
const sortTestCase = async (apiParams?: TestCaseSearchParams) => {
const updatedValue = uniq([...selectedFilter, ...Object.keys(params)]);
await fetchTestCases(
INITIAL_PAGING_VALUE,
updatedValue,
apiParams ?? DEFAULT_SORT_ORDER
);
setSortOptions(apiParams ?? DEFAULT_SORT_ORDER);
};
const handleStatusSubmit = (testCase: TestCase) => {
setTestCase((prev) => {
const data = prev.map((test) => {
@ -620,6 +637,7 @@ export const TestCases = ({ summaryPanel }: { summaryPanel: ReactNode }) => {
url: getDataQualityPagePath(DataQualityPageTabs.TEST_CASES),
},
]}
fetchTestCases={sortTestCase}
isLoading={isLoading}
pagingData={pagingData}
showPagination={showPagination}

View File

@ -27,6 +27,7 @@ import DataQualityTab from './DataQualityTab';
const mockProps: DataQualityTabProps = {
testCases: MOCK_TEST_CASE,
onTestUpdate: jest.fn(),
fetchTestCases: jest.fn(),
};
const mockPermissionsData = {
permissions: {
@ -114,6 +115,76 @@ describe('DataQualityTab test', () => {
expect(await findByText(header, 'label.action-plural')).toBeInTheDocument();
});
it('Should send API request with sort params on click of last-run', async () => {
await act(async () => {
render(<DataQualityTab {...mockProps} />);
});
const tableRows = await screen.findAllByRole('row');
const header = tableRows[0];
const lastRunHeader = await findByText(header, 'label.last-run');
expect(lastRunHeader).toBeInTheDocument();
await act(async () => {
fireEvent.click(lastRunHeader);
});
expect(mockProps.fetchTestCases).toHaveBeenCalledWith({
sortField: 'testCaseResult.timestamp',
sortType: 'asc',
});
await act(async () => {
fireEvent.click(lastRunHeader);
});
expect(mockProps.fetchTestCases).toHaveBeenCalledWith({
sortField: 'testCaseResult.timestamp',
sortType: 'desc',
});
await act(async () => {
fireEvent.click(lastRunHeader);
});
expect(mockProps.fetchTestCases).toHaveBeenCalledWith(undefined);
});
it('Should send API request with sort params on click of name', async () => {
await act(async () => {
render(<DataQualityTab {...mockProps} />);
});
const tableRows = await screen.findAllByRole('row');
const header = tableRows[0];
const lastRunHeader = await findByText(header, 'label.name');
expect(lastRunHeader).toBeInTheDocument();
await act(async () => {
fireEvent.click(lastRunHeader);
});
expect(mockProps.fetchTestCases).toHaveBeenCalledWith({
sortField: 'name.keyword',
sortType: 'asc',
});
await act(async () => {
fireEvent.click(lastRunHeader);
});
expect(mockProps.fetchTestCases).toHaveBeenCalledWith({
sortField: 'name.keyword',
sortType: 'desc',
});
await act(async () => {
fireEvent.click(lastRunHeader);
});
expect(mockProps.fetchTestCases).toHaveBeenCalledWith(undefined);
});
it('Table data should be render as per data props', async () => {
const firstRowData = MOCK_TEST_CASE[0];
await act(async () => {

View File

@ -12,13 +12,14 @@
*/
import { Button, Col, Row, Skeleton, Space, Tooltip, Typography } from 'antd';
import { ColumnsType } from 'antd/lib/table';
import { ColumnsType, TablePaginationConfig } from 'antd/lib/table';
import { FilterValue, SorterResult } from 'antd/lib/table/interface';
import { AxiosError } from 'axios';
import classNames from 'classnames';
import { isUndefined, sortBy } from 'lodash';
import { isArray, isUndefined, sortBy } from 'lodash';
import { PagingResponse } from 'Models';
import QueryString from 'qs';
import React, { useEffect, useMemo, useState } from 'react';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import { ReactComponent as IconEdit } from '../../../../assets/svg/edit-new.svg';
@ -28,10 +29,14 @@ import { DATA_QUALITY_PROFILER_DOCS } from '../../../../constants/docs.constants
import { NO_PERMISSION_FOR_ACTION } from '../../../../constants/HelperTextUtil';
import { usePermissionProvider } from '../../../../context/PermissionProvider/PermissionProvider';
import { ResourceEntity } from '../../../../context/PermissionProvider/PermissionProvider.interface';
import { SORT_ORDER } from '../../../../enums/common.enum';
import { EntityTabs, EntityType } from '../../../../enums/entity.enum';
import { TestCaseStatus } from '../../../../generated/configuration/testResultNotificationConfiguration';
import { Operation } from '../../../../generated/entity/policies/policy';
import { TestCase, TestCaseResult } from '../../../../generated/tests/testCase';
import {
TestCase,
TestCaseResult,
TestCaseStatus,
} from '../../../../generated/tests/testCase';
import { TestCaseResolutionStatus } from '../../../../generated/tests/testCaseResolutionStatus';
import { getListTestCaseIncidentByStateId } from '../../../../rest/incidentManagerAPI';
import { removeTestCaseFromTestSuite } from '../../../../rest/testAPI';
@ -74,6 +79,7 @@ const DataQualityTab: React.FC<DataQualityTabProps> = ({
afterDeleteAction,
showPagination,
breadcrumbData,
fetchTestCases,
}) => {
const { t } = useTranslation();
const { permissions } = usePermissionProvider();
@ -82,6 +88,7 @@ const DataQualityTab: React.FC<DataQualityTabProps> = ({
const [testCaseStatus, setTestCaseStatus] = useState<
TestCaseResolutionStatus[]
>([]);
const isApiSortingEnabled = useRef(false);
const testCaseEditPermission = useMemo(() => {
return checkPermission(
@ -101,19 +108,21 @@ const DataQualityTab: React.FC<DataQualityTabProps> = ({
const sortedData = useMemo(
() =>
sortBy(testCases, (test) => {
switch (test.testCaseResult?.testCaseStatus) {
case TestCaseStatus.Failed:
return 0;
case TestCaseStatus.Aborted:
return 1;
case TestCaseStatus.Success:
return 2;
isApiSortingEnabled.current
? testCases
: sortBy(testCases, (test) => {
switch (test.testCaseResult?.testCaseStatus) {
case TestCaseStatus.Failed:
return 0;
case TestCaseStatus.Aborted:
return 1;
case TestCaseStatus.Success:
return 2;
default:
return 3;
}
}),
default:
return 3;
}
}),
[testCases]
);
@ -144,7 +153,7 @@ const DataQualityTab: React.FC<DataQualityTabProps> = ({
dataIndex: 'name',
key: 'name',
width: 300,
sorter: (a, b) => a.name.localeCompare(b.name),
sorter: true,
sortDirections: ['ascend', 'descend'],
render: (name: string, record) => {
const status = record.testCaseResult?.testCaseStatus;
@ -247,6 +256,7 @@ const DataQualityTab: React.FC<DataQualityTabProps> = ({
dataIndex: 'testCaseResult',
key: 'lastRun',
width: 150,
sorter: true,
render: (result: TestCaseResult) =>
result?.timestamp ? formatDateTime(result.timestamp) : '--',
},
@ -411,6 +421,29 @@ const DataQualityTab: React.FC<DataQualityTabProps> = ({
}
};
const handleTableChange = (
_pagination: TablePaginationConfig,
_filters: Record<string, FilterValue | null>,
sorter: SorterResult<TestCase> | SorterResult<TestCase>[]
) => {
if (!isArray(sorter) && fetchTestCases) {
if (sorter?.columnKey === 'lastRun' || sorter?.columnKey === 'name') {
const sortData = isUndefined(sorter.order)
? undefined
: {
sortField:
sorter?.columnKey === 'lastRun'
? 'testCaseResult.timestamp'
: 'name.keyword',
sortType:
sorter?.order === 'ascend' ? SORT_ORDER.ASC : SORT_ORDER.DESC,
};
isApiSortingEnabled.current = !isUndefined(sorter.order);
fetchTestCases(sortData);
}
}
};
useEffect(() => {
if (testCases.length) {
fetchTestCaseStatus();
@ -454,6 +487,7 @@ const DataQualityTab: React.FC<DataQualityTabProps> = ({
pagination={false}
rowKey="id"
size="small"
onChange={handleTableChange}
/>
</Col>
<Col span={24}>

View File

@ -21,7 +21,10 @@ import {
import { Thread } from '../../../../generated/entity/feed/thread';
import { TestCase } from '../../../../generated/tests/testCase';
import { TestSuite } from '../../../../generated/tests/testSuite';
import { ListTestCaseParams } from '../../../../rest/testAPI';
import {
ListTestCaseParams,
ListTestCaseParamsBySearch,
} from '../../../../rest/testAPI';
import { NextPreviousProps } from '../../../common/NextPrevious/NextPrevious.interface';
import { TitleBreadcrumbProps } from '../../../common/TitleBreadcrumb/TitleBreadcrumb.interface';
@ -115,6 +118,7 @@ export interface DataQualityTabProps {
};
showPagination?: boolean;
breadcrumbData?: TitleBreadcrumbProps['titleLinks'];
fetchTestCases?: (params?: ListTestCaseParamsBySearch) => Promise<void>;
}
export interface TestSummaryProps {

View File

@ -16,9 +16,13 @@ import { isEmpty } from 'lodash';
import React, { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useHistory } from 'react-router-dom';
import { getEntityDetailsPath } from '../../../../../constants/constants';
import {
getEntityDetailsPath,
INITIAL_PAGING_VALUE,
} from '../../../../../constants/constants';
import { PAGE_HEADERS } from '../../../../../constants/PageHeaders.constant';
import {
DEFAULT_SORT_ORDER,
TEST_CASE_STATUS_OPTION,
TEST_CASE_TYPE_OPTION,
} from '../../../../../constants/profiler.constant';
@ -29,7 +33,10 @@ import { ProfilerDashboardType } from '../../../../../enums/table.enum';
import { TestCaseStatus } from '../../../../../generated/tests/testCase';
import LimitWrapper from '../../../../../hoc/LimitWrapper';
import { useFqn } from '../../../../../hooks/useFqn';
import { TestCaseType } from '../../../../../rest/testAPI';
import {
ListTestCaseParamsBySearch,
TestCaseType,
} from '../../../../../rest/testAPI';
import {
getBreadcrumbForTable,
getEntityName,
@ -78,6 +85,8 @@ export const QualityTab = () => {
useState<TestCaseStatus>('' as TestCaseStatus);
const [selectedTestType, setSelectedTestType] = useState(TestCaseType.all);
const [searchValue, setSearchValue] = useState<string>();
const [sortOptions, setSortOptions] =
useState<ListTestCaseParamsBySearch>(DEFAULT_SORT_ORDER);
const testSuite = useMemo(() => table?.testSuite, [table]);
const handleTestCasePageChange: NextPreviousProps['pagingHandler'] = ({
@ -85,6 +94,7 @@ export const QualityTab = () => {
}) => {
if (currentPage) {
fetchAllTests({
...sortOptions,
testCaseType: selectedTestType,
testCaseStatus: isEmpty(selectedTestCaseStatus)
? undefined
@ -106,6 +116,12 @@ export const QualityTab = () => {
});
};
const handleSortTestCase = async (apiParams?: ListTestCaseParamsBySearch) => {
setSortOptions(apiParams ?? DEFAULT_SORT_ORDER);
await fetchAllTests({ ...(apiParams ?? DEFAULT_SORT_ORDER), offset: 0 });
handlePageChange(INITIAL_PAGING_VALUE);
};
const tableBreadcrumb = useMemo(() => {
return table
? [
@ -147,6 +163,7 @@ export const QualityTab = () => {
(await getResourceLimit('dataQuality', true, true));
}}
breadcrumbData={tableBreadcrumb}
fetchTestCases={handleSortTestCase}
isLoading={isTestsLoading}
showTableColumn={false}
testCases={allTestCases}

View File

@ -165,6 +165,8 @@ describe('QualityTab', () => {
offset: 10,
testCaseStatus: undefined,
testCaseType: 'all',
sortField: 'testCaseResult.timestamp',
sortType: 'desc',
});
});

View File

@ -26,7 +26,10 @@ import React, {
import { useTranslation } from 'react-i18next';
import { PAGE_SIZE } from '../../../../constants/constants';
import { mockDatasetData } from '../../../../constants/mockTourData.constants';
import { DEFAULT_RANGE_DATA } from '../../../../constants/profiler.constant';
import {
DEFAULT_RANGE_DATA,
DEFAULT_SORT_ORDER,
} from '../../../../constants/profiler.constant';
import { useTourProvider } from '../../../../context/TourProvider/TourProvider';
import { TabSpecificField } from '../../../../enums/entity.enum';
import { Table } from '../../../../generated/entity/data/table';
@ -209,6 +212,7 @@ export const TableProfilerProvider = ({
setIsTestsLoading(true);
try {
const { data, paging } = await getListTestCaseBySearch({
...DEFAULT_SORT_ORDER,
...params,
fields: [
TabSpecificField.TEST_CASE_RESULT,

View File

@ -16,6 +16,7 @@ import { capitalize, map, startCase, values } from 'lodash';
import { DateFilterType, StepperStepType } from 'Models';
import { TestCaseSearchParams } from '../components/DataQuality/DataQuality.interface';
import { CSMode } from '../enums/codemirror.enum';
import { SORT_ORDER } from '../enums/common.enum';
import { DMLOperationType } from '../generated/api/data/createTableProfile';
import {
ColumnProfilerConfig,
@ -466,3 +467,8 @@ export const DEFAULT_PROFILER_CONFIG_VALUE = {
},
],
};
export const DEFAULT_SORT_ORDER = {
sortType: SORT_ORDER.DESC,
sortField: 'testCaseResult.timestamp',
};

View File

@ -31,6 +31,8 @@ import { TitleBreadcrumbProps } from '../../components/common/TitleBreadcrumb/Ti
import DataQualityTab from '../../components/Database/Profiler/DataQualityTab/DataQualityTab';
import { AddTestCaseList } from '../../components/DataQuality/AddTestCaseList/AddTestCaseList.component';
import PageLayoutV1 from '../../components/PageLayoutV1/PageLayoutV1';
import { INITIAL_PAGING_VALUE } from '../../constants/constants';
import { DEFAULT_SORT_ORDER } from '../../constants/profiler.constant';
import { usePermissionProvider } from '../../context/PermissionProvider/PermissionProvider';
import {
OperationPermission,
@ -86,10 +88,12 @@ const TestSuiteDetailsPage = () => {
showPagination,
} = usePaging();
const [isLoading, setIsLoading] = useState<boolean>(false);
const [testSuitePermissions, setTestSuitePermission] =
const [testSuitePermissions, setTestSuitePermissions] =
useState<OperationPermission>(DEFAULT_ENTITY_PERMISSION);
const [isTestCaseModalOpen, setIsTestCaseModalOpen] =
useState<boolean>(false);
const [sortOptions, setSortOptions] =
useState<ListTestCaseParamsBySearch>(DEFAULT_SORT_ORDER);
const [slashedBreadCrumb, setSlashedBreadCrumb] = useState<
TitleBreadcrumbProps['titleLinks']
@ -133,7 +137,7 @@ const TestSuiteDetailsPage = () => {
ResourceEntity.TEST_SUITE,
testSuiteFQN
);
setTestSuitePermission(response);
setTestSuitePermissions(response);
} catch (error) {
showErrorToast(error as AxiosError);
} finally {
@ -152,6 +156,7 @@ const TestSuiteDetailsPage = () => {
TabSpecificField.INCIDENT_ID,
],
testSuiteId,
...sortOptions,
...param,
limit: pageSize,
});
@ -169,6 +174,11 @@ const TestSuiteDetailsPage = () => {
setIsTestCaseLoading(false);
}
};
const handleSortTestCase = async (apiParams?: ListTestCaseParamsBySearch) => {
setSortOptions(apiParams ?? DEFAULT_SORT_ORDER);
await fetchTestCases({ ...(apiParams ?? DEFAULT_SORT_ORDER), offset: 0 });
handlePageChange(INITIAL_PAGING_VALUE);
};
const handleAddTestCaseSubmit = async (testCases: TestCase[]) => {
const testCaseIds = testCases.reduce((ids, curr) => {
@ -391,6 +401,7 @@ const TestSuiteDetailsPage = () => {
<DataQualityTab
afterDeleteAction={fetchTestCases}
breadcrumbData={incidentUrlState}
fetchTestCases={handleSortTestCase}
isLoading={isLoading || isTestCaseLoading}
pagingData={pagingData}
removeFromTestSuite={{ testSuite: testSuite as TestSuite }}