feat(ui): support tag & tier filter for test case (#16502)

* feat(ui): support tag & tier filter for test case

* fix tag filter

* allow single select for tier

* added service name filter

* update cypress for tags, tier & service

* add specific add for filters

* fix tier api call
This commit is contained in:
Chirag Madlani 2024-06-04 15:44:46 +05:30 committed by GitHub
parent a85fc43f5e
commit 5b71d79e8a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 348 additions and 39 deletions

View File

@ -299,6 +299,40 @@ export const prepareDataQualityTestCases = (token: string) => {
); );
}); });
cy.request({
method: 'PATCH',
url: `/api/v1/tables/name/${filterTable.databaseSchema}.${filterTable.name}`,
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json-patch+json',
},
body: [
{
op: 'add',
path: '/tags/0',
value: {
tagFQN: 'PII.None',
name: 'None',
description: 'Non PII',
source: 'Classification',
labelType: 'Manual',
state: 'Confirmed',
},
},
{
op: 'add',
path: '/tags/1',
value: {
tagFQN: 'Tier.Tier2',
name: 'Tier2',
source: 'Classification',
labelType: 'Manual',
state: 'Confirmed',
},
},
],
});
prepareDataQualityTestCasesViaREST({ prepareDataQualityTestCasesViaREST({
testSuite: filterTableTestSuite, testSuite: filterTableTestSuite,
token, token,

View File

@ -106,6 +106,16 @@ const verifyFilterTestCase = () => {
cy.get(`[data-testid="${testCase}"]`).scrollIntoView().should('be.visible'); cy.get(`[data-testid="${testCase}"]`).scrollIntoView().should('be.visible');
}); });
}; };
const verifyFilter2TestCase = (negation = false) => {
filterTable2TestCases.map((testCase) => {
negation
? cy.get(`[data-testid="${testCase}"]`).should('not.exist')
: cy
.get(`[data-testid="${testCase}"]`)
.scrollIntoView()
.should('be.visible');
});
};
describe( describe(
'Data Quality and Profiler should work properly', 'Data Quality and Profiler should work properly',
@ -967,6 +977,12 @@ describe(
'getTestCase' 'getTestCase'
); );
interceptURL(
'GET',
`/api/v1/search/query?q=*index=tag_search_index*`,
'searchTags'
);
cy.sidebarClick(SidebarItem.DATA_QUALITY); cy.sidebarClick(SidebarItem.DATA_QUALITY);
cy.get('[data-testid="by-test-cases"]').click(); cy.get('[data-testid="by-test-cases"]').click();
@ -988,6 +1004,19 @@ describe(
waitForAnimations: true, waitForAnimations: true,
}); });
cy.get('[value="lastRunRange"]').click({ waitForAnimations: true }); cy.get('[value="lastRunRange"]').click({ waitForAnimations: true });
cy.get('[data-testid="advanced-filter"]').click({
waitForAnimations: true,
});
cy.get('[value="serviceName"]').click({ waitForAnimations: true });
cy.get('[data-testid="advanced-filter"]').click({
waitForAnimations: true,
});
cy.get('[value="tags"]').click({ waitForAnimations: true });
cy.get('[data-testid="advanced-filter"]').click({
waitForAnimations: true,
});
cy.get('[value="tier"]').click({ waitForAnimations: true });
// Test case search filter // Test case search filter
cy.get( cy.get(
@ -1000,6 +1029,78 @@ describe(
cy.get('.ant-input-clear-icon').click(); cy.get('.ant-input-clear-icon').click();
verifyResponseStatusCode('@getTestCase', 200); verifyResponseStatusCode('@getTestCase', 200);
// Test case filter by service name
interceptURL(
'GET',
`/api/v1/dataQuality/testCases/search/list?*serviceName=${DATABASE_SERVICE.service.name}*`,
'getTestCaseByServiceName'
);
interceptURL(
'GET',
`/api/v1/search/query?q=*index=database_service_search_index*`,
'searchService'
);
cy.get('#serviceName')
.scrollIntoView()
.type(DATABASE_SERVICE.service.name);
verifyResponseStatusCode('@searchService', 200);
cy.get('.ant-select-dropdown')
.not('.ant-select-dropdown-hidden')
.find(`[data-testid="${DATABASE_SERVICE.service.name}"]`)
.click({ force: true });
verifyResponseStatusCode('@getTestCaseByServiceName', 200);
verifyFilterTestCase();
verifyFilter2TestCase();
// remove service filter
cy.get('[data-testid="advanced-filter"]').click({
waitForAnimations: true,
});
cy.get('[value="serviceName"]').click({ waitForAnimations: true });
verifyResponseStatusCode('@getTestCase', 200);
// Test case filter by Tags
interceptURL(
'GET',
`/api/v1/dataQuality/testCases/search/list?*tags=${'PII.None'}*`,
'getTestCaseByTags'
);
cy.get('#tags').scrollIntoView().click().type('PII.None');
verifyResponseStatusCode('@searchTags', 200);
cy.get('.ant-select-dropdown')
.not('.ant-select-dropdown-hidden')
.find(`[data-testid="${'PII.None'}"]`)
.click({ force: true });
verifyResponseStatusCode('@getTestCaseByTags', 200);
verifyFilterTestCase();
verifyFilter2TestCase(true);
// remove service filter
cy.get('[data-testid="advanced-filter"]').click({
waitForAnimations: true,
});
cy.get('[value="tags"]').click({ waitForAnimations: true });
verifyResponseStatusCode('@getTestCase', 200);
// Test case filter by Tier
interceptURL(
'GET',
`/api/v1/dataQuality/testCases/search/list?*tier=${'Tier.Tier2'}*`,
'getTestCaseByTier'
);
cy.get('#tier').click();
cy.get('.ant-select-dropdown')
.not('.ant-select-dropdown-hidden')
.find(`[data-testid="${'Tier.Tier2'}"]`)
.click({ force: true });
verifyResponseStatusCode('@getTestCaseByTier', 200);
verifyFilterTestCase();
verifyFilter2TestCase(true);
// remove service filter
cy.get('[data-testid="advanced-filter"]').click({
waitForAnimations: true,
});
cy.get('[value="tier"]').click({ waitForAnimations: true });
verifyResponseStatusCode('@getTestCase', 200);
// Test case filter by table name // Test case filter by table name
interceptURL( interceptURL(
'GET', 'GET',
@ -1021,10 +1122,7 @@ describe(
.click({ force: true }); .click({ force: true });
verifyResponseStatusCode('@searchTestCaseByTable', 200); verifyResponseStatusCode('@searchTestCaseByTable', 200);
verifyFilterTestCase(); verifyFilterTestCase();
verifyFilter2TestCase(true);
filterTable2TestCases.map((testCase) => {
cy.get(`[data-testid="${testCase}"]`).should('not.exist');
});
// Test case filter by test type // Test case filter by test type
interceptURL( interceptURL(

View File

@ -51,6 +51,7 @@ import {
INITIAL_PAGING_VALUE, INITIAL_PAGING_VALUE,
PAGE_SIZE, PAGE_SIZE,
PAGE_SIZE_BASE, PAGE_SIZE_BASE,
TIER_CATEGORY,
} from '../../../constants/constants'; } from '../../../constants/constants';
import { import {
TEST_CASE_FILTERS, TEST_CASE_FILTERS,
@ -65,6 +66,7 @@ import { TestCase } from '../../../generated/tests/testCase';
import { usePaging } from '../../../hooks/paging/usePaging'; import { usePaging } from '../../../hooks/paging/usePaging';
import { DataQualityPageTabs } from '../../../pages/DataQuality/DataQualityPage.interface'; import { DataQualityPageTabs } from '../../../pages/DataQuality/DataQualityPage.interface';
import { searchQuery } from '../../../rest/searchAPI'; import { searchQuery } from '../../../rest/searchAPI';
import { getTags } from '../../../rest/tagAPI';
import { import {
getListTestCaseBySearch, getListTestCaseBySearch,
ListTestCaseParamsBySearch, ListTestCaseParamsBySearch,
@ -73,6 +75,7 @@ import { buildTestCaseParams } from '../../../utils/DataQuality/DataQualityUtils
import { getEntityName } from '../../../utils/EntityUtils'; import { getEntityName } from '../../../utils/EntityUtils';
import { getDataQualityPagePath } from '../../../utils/RouterUtils'; import { getDataQualityPagePath } from '../../../utils/RouterUtils';
import { generateEntityLink } from '../../../utils/TableUtils'; import { generateEntityLink } from '../../../utils/TableUtils';
import tagClassBase from '../../../utils/TagClassBase';
import { showErrorToast } from '../../../utils/ToastUtils'; import { showErrorToast } from '../../../utils/ToastUtils';
import DatePickerMenu from '../../common/DatePickerMenu/DatePickerMenu.component'; import DatePickerMenu from '../../common/DatePickerMenu/DatePickerMenu.component';
import ErrorPlaceHolder from '../../common/ErrorWithPlaceholder/ErrorPlaceHolder'; import ErrorPlaceHolder from '../../common/ErrorWithPlaceholder/ErrorPlaceHolder';
@ -90,7 +93,10 @@ export const TestCases = ({ summaryPanel }: { summaryPanel: ReactNode }) => {
const { permissions } = usePermissionProvider(); const { permissions } = usePermissionProvider();
const { testCase: testCasePermission } = permissions; const { testCase: testCasePermission } = permissions;
const [tableOptions, setTableOptions] = useState<DefaultOptionType[]>([]); const [tableOptions, setTableOptions] = useState<DefaultOptionType[]>([]);
const [isTableLoading, setIsTableLoading] = useState(false); const [isOptionsLoading, setIsOptionsLoading] = useState(false);
const [tagOptions, setTagOptions] = useState<DefaultOptionType[]>([]);
const [tierOptions, setTierOptions] = useState<DefaultOptionType[]>([]);
const [serviceOptions, setServiceOptions] = useState<DefaultOptionType[]>([]);
const params = useMemo(() => { const params = useMemo(() => {
const search = location.search; const search = location.search;
@ -204,6 +210,7 @@ export const TestCases = ({ summaryPanel }: { summaryPanel: ReactNode }) => {
buildTestCaseParams(params, selectedFilter), buildTestCaseParams(params, selectedFilter),
isUndefined isUndefined
); );
if (!isEqual(filters, updatedParams)) { if (!isEqual(filters, updatedParams)) {
fetchTestCases(INITIAL_PAGING_VALUE, updatedParams); fetchTestCases(INITIAL_PAGING_VALUE, updatedParams);
} }
@ -211,40 +218,74 @@ export const TestCases = ({ summaryPanel }: { summaryPanel: ReactNode }) => {
setFilters(updatedParams); setFilters(updatedParams);
}; };
const handleMenuClick = ({ key }: { key: string }) => { const fetchTierOptions = async () => {
setSelectedFilter((prevSelected) => { try {
if (prevSelected.includes(key)) { setIsOptionsLoading(true);
const updatedValue = prevSelected.filter( const { data } = await getTags({
(selected) => selected !== key parent: 'Tier',
); });
const updatedFilters = omitBy(
buildTestCaseParams(filters, updatedValue),
isUndefined
);
form.setFieldsValue({ [key]: undefined });
if (!isEqual(filters, updatedFilters)) {
fetchTestCases(INITIAL_PAGING_VALUE, updatedFilters);
}
setFilters(updatedFilters);
return updatedValue; const options = data.map((hit) => {
} return {
label: (
<Space
data-testid={hit.fullyQualifiedName}
direction="vertical"
size={0}>
<Typography.Text className="text-xs text-grey-muted">
{hit.fullyQualifiedName}
</Typography.Text>
<Typography.Text className="text-sm">
{getEntityName(hit)}
</Typography.Text>
</Space>
),
value: hit.fullyQualifiedName,
};
});
return [...prevSelected, key]; setTierOptions(options);
}); } catch (error) {
setTierOptions([]);
} finally {
setIsOptionsLoading(false);
}
}; };
const filterMenu: ItemType[] = useMemo(() => { const fetchTagOptions = async (search?: string) => {
return entries(TEST_CASE_FILTERS).map(([name, filter]) => ({ setIsOptionsLoading(true);
key: filter, try {
label: startCase(name), const { data } = await tagClassBase.getTags(search ?? '', 1);
value: filter,
onClick: handleMenuClick, const options = data
})); .filter(
}, [filters]); ({ data: { classification } }) =>
classification?.name !== TIER_CATEGORY
)
.map(({ label, value }) => {
return {
label: (
<Space data-testid={value} direction="vertical" size={0}>
<Typography.Text className="text-xs text-grey-muted">
{value}
</Typography.Text>
<Typography.Text className="text-sm">{label}</Typography.Text>
</Space>
),
value: value,
};
});
setTagOptions(options);
} catch (error) {
setTagOptions([]);
} finally {
setIsOptionsLoading(false);
}
};
const fetchTableData = async (search = WILD_CARD_CHAR) => { const fetchTableData = async (search = WILD_CARD_CHAR) => {
setIsTableLoading(true); setIsOptionsLoading(true);
try { try {
const response = await searchQuery({ const response = await searchQuery({
query: `*${search}*`, query: `*${search}*`,
@ -277,14 +318,99 @@ export const TestCases = ({ summaryPanel }: { summaryPanel: ReactNode }) => {
} catch (error) { } catch (error) {
setTableOptions([]); setTableOptions([]);
} finally { } finally {
setIsTableLoading(false); setIsOptionsLoading(false);
} }
}; };
const fetchServiceOptions = async (search = WILD_CARD_CHAR) => {
setIsOptionsLoading(true);
try {
const response = await searchQuery({
query: `*${search}*`,
pageNumber: 1,
pageSize: PAGE_SIZE_BASE,
searchIndex: SearchIndex.DATABASE_SERVICE,
fetchSource: true,
includeFields: ['name', 'fullyQualifiedName', 'displayName'],
});
const options = response.hits.hits.map((hit) => {
return {
label: (
<Space
data-testid={hit._source.fullyQualifiedName}
direction="vertical"
size={0}>
<Typography.Text className="text-xs text-grey-muted">
{hit._source.fullyQualifiedName}
</Typography.Text>
<Typography.Text className="text-sm">
{getEntityName(hit._source)}
</Typography.Text>
</Space>
),
value: hit._source.fullyQualifiedName,
};
});
setServiceOptions(options);
} catch (error) {
setServiceOptions([]);
} finally {
setIsOptionsLoading(false);
}
};
const handleMenuClick = ({ key }: { key: string }) => {
setSelectedFilter((prevSelected) => {
if (prevSelected.includes(key)) {
const updatedValue = prevSelected.filter(
(selected) => selected !== key
);
const updatedFilters = omitBy(
buildTestCaseParams(filters, updatedValue),
isUndefined
);
form.setFieldsValue({ [key]: undefined });
if (!isEqual(filters, updatedFilters)) {
fetchTestCases(INITIAL_PAGING_VALUE, updatedFilters);
}
setFilters(updatedFilters);
return updatedValue;
}
return [...prevSelected, key];
});
// Fetch options based on the selected filter
key === TEST_CASE_FILTERS.tier && fetchTierOptions();
key === TEST_CASE_FILTERS.tags && fetchTagOptions();
key === TEST_CASE_FILTERS.table && fetchTableData();
key === TEST_CASE_FILTERS.service && fetchServiceOptions();
};
const filterMenu: ItemType[] = useMemo(() => {
return entries(TEST_CASE_FILTERS).map(([name, filter]) => ({
key: filter,
label: startCase(name),
value: filter,
onClick: handleMenuClick,
}));
}, [filters]);
const debounceFetchTableData = useCallback(debounce(fetchTableData, 1000), [ const debounceFetchTableData = useCallback(debounce(fetchTableData, 1000), [
fetchTableData, fetchTableData,
]); ]);
const debounceFetchTagOptions = useCallback(debounce(fetchTagOptions, 1000), [
fetchTagOptions,
]);
const debounceFetchServiceOptions = useCallback(
debounce(fetchServiceOptions, 1000),
[fetchServiceOptions]
);
useEffect(() => { useEffect(() => {
if (testCasePermission?.ViewAll || testCasePermission?.ViewBasic) { if (testCasePermission?.ViewAll || testCasePermission?.ViewBasic) {
if (tab === DataQualityPageTabs.TEST_CASES) { if (tab === DataQualityPageTabs.TEST_CASES) {
@ -295,10 +421,6 @@ export const TestCases = ({ summaryPanel }: { summaryPanel: ReactNode }) => {
} }
}, [tab, searchValue, testCasePermission, pageSize]); }, [tab, searchValue, testCasePermission, pageSize]);
useEffect(() => {
fetchTableData();
}, []);
const pagingData = useMemo( const pagingData = useMemo(
() => ({ () => ({
paging, paging,
@ -363,7 +485,7 @@ export const TestCases = ({ summaryPanel }: { summaryPanel: ReactNode }) => {
allowClear allowClear
showSearch showSearch
data-testid="table-select-filter" data-testid="table-select-filter"
loading={isTableLoading} loading={isOptionsLoading}
options={tableOptions} options={tableOptions}
placeholder={t('label.table')} placeholder={t('label.table')}
onSearch={debounceFetchTableData} onSearch={debounceFetchTableData}
@ -420,6 +542,52 @@ export const TestCases = ({ summaryPanel }: { summaryPanel: ReactNode }) => {
<DatePickerMenu showSelectedCustomRange /> <DatePickerMenu showSelectedCustomRange />
</Form.Item> </Form.Item>
)} )}
{selectedFilter.includes(TEST_CASE_FILTERS.tags) && (
<Form.Item
className="m-0 w-80"
label={t('label.tag-plural')}
name="tags">
<Select
allowClear
showSearch
data-testid="tags-select-filter"
loading={isOptionsLoading}
mode="multiple"
options={tagOptions}
placeholder={t('label.tag-plural')}
onSearch={debounceFetchTagOptions}
/>
</Form.Item>
)}
{selectedFilter.includes(TEST_CASE_FILTERS.tier) && (
<Form.Item
className="m-0 w-40"
label={t('label.tier')}
name="tier">
<Select
allowClear
data-testid="tier-select-filter"
options={tierOptions}
placeholder={t('label.tier')}
/>
</Form.Item>
)}
{selectedFilter.includes(TEST_CASE_FILTERS.service) && (
<Form.Item
className="m-0 w-80"
label={t('label.service')}
name="serviceName">
<Select
allowClear
showSearch
data-testid="service-select-filter"
loading={isOptionsLoading}
options={serviceOptions}
placeholder={t('label.service')}
onSearch={debounceFetchServiceOptions}
/>
</Form.Item>
)}
</Space> </Space>
</Form> </Form>
</Col> </Col>

View File

@ -423,6 +423,9 @@ export const TEST_CASE_FILTERS = {
type: 'testCaseType', type: 'testCaseType',
status: 'testCaseStatus', status: 'testCaseStatus',
lastRun: 'lastRunRange', lastRun: 'lastRunRange',
tier: 'tier',
tags: 'tags',
service: 'serviceName',
}; };
export const TEST_CASE_PLATFORM_OPTION = values(TestPlatform).map((value) => ({ export const TEST_CASE_PLATFORM_OPTION = values(TestPlatform).map((value) => ({

View File

@ -74,6 +74,9 @@ export type ListTestCaseParamsBySearch = ListTestCaseParams & {
testPlatforms?: TestPlatform[]; testPlatforms?: TestPlatform[];
offset?: number; offset?: number;
owner?: string; owner?: string;
tags?: string;
tier?: string;
serviceName?: string;
}; };
export type ListTestDefinitionsParams = ListParams & { export type ListTestDefinitionsParams = ListParams & {

View File

@ -39,5 +39,8 @@ export const buildTestCaseParams = (
...filterParams('testPlatforms', TEST_CASE_FILTERS.platform), ...filterParams('testPlatforms', TEST_CASE_FILTERS.platform),
...filterParams('testCaseType', TEST_CASE_FILTERS.type), ...filterParams('testCaseType', TEST_CASE_FILTERS.type),
...filterParams('testCaseStatus', TEST_CASE_FILTERS.status), ...filterParams('testCaseStatus', TEST_CASE_FILTERS.status),
...filterParams('tags', TEST_CASE_FILTERS.tags),
...filterParams('tier', TEST_CASE_FILTERS.tier),
...filterParams('serviceName', TEST_CASE_FILTERS.service),
}; };
}; };