mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-08-26 18:06:03 +00:00
fix custom property type not coming in advanced search (#17573)
* fix custom property type not coming in advance search * added playwright test to test dashboard for same * minor code optimization and added comments in code
This commit is contained in:
parent
ce19fa3389
commit
d81a57c44e
@ -0,0 +1,158 @@
|
||||
/*
|
||||
* Copyright 2024 Collate.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { CUSTOM_PROPERTIES_ENTITIES } from '../../../constant/customProperty';
|
||||
import { GlobalSettingOptions } from '../../../constant/settings';
|
||||
import { SidebarItem } from '../../../constant/sidebar';
|
||||
import { DashboardClass } from '../../../support/entity/DashboardClass';
|
||||
import { createNewPage, redirectToHomePage, uuid } from '../../../utils/common';
|
||||
import {
|
||||
addCustomPropertiesForEntity,
|
||||
deleteCreatedProperty,
|
||||
} from '../../../utils/customProperty';
|
||||
import { settingClick, sidebarClick } from '../../../utils/sidebar';
|
||||
|
||||
// use the admin user to login
|
||||
test.use({ storageState: 'playwright/.auth/admin.json' });
|
||||
|
||||
const dashboardEntity = new DashboardClass();
|
||||
const propertyName = `pwCustomPropertyDashboardTest${uuid()}`;
|
||||
const propertyValue = 'dashboardcustomproperty';
|
||||
|
||||
test.beforeAll('Setup pre-requests', async ({ browser }) => {
|
||||
const { apiContext, afterAction } = await createNewPage(browser);
|
||||
await dashboardEntity.create(apiContext);
|
||||
await afterAction();
|
||||
});
|
||||
|
||||
test.afterAll('Cleanup', async ({ browser }) => {
|
||||
const { apiContext, afterAction } = await createNewPage(browser);
|
||||
await dashboardEntity.delete(apiContext);
|
||||
await afterAction();
|
||||
});
|
||||
|
||||
test('CustomProperty Dashboard Filter', async ({ page }) => {
|
||||
test.slow(true);
|
||||
|
||||
await redirectToHomePage(page);
|
||||
|
||||
await test.step('Create Dashboard Custom Property', async () => {
|
||||
await settingClick(page, GlobalSettingOptions.DASHBOARDS, true);
|
||||
|
||||
await addCustomPropertiesForEntity({
|
||||
page,
|
||||
propertyName,
|
||||
customPropertyData: CUSTOM_PROPERTIES_ENTITIES['entity_dashboard'],
|
||||
customType: 'String',
|
||||
});
|
||||
});
|
||||
|
||||
await test.step('Add Custom Property in Dashboard', async () => {
|
||||
await dashboardEntity.visitEntityPage(page);
|
||||
|
||||
await page.getByTestId('custom_properties').click();
|
||||
|
||||
await page
|
||||
.getByRole('row', { name: `${propertyName} No data` })
|
||||
.locator('svg')
|
||||
.click();
|
||||
|
||||
await page.getByTestId('value-input').fill(propertyValue);
|
||||
|
||||
const saveResponse = page.waitForResponse('/api/v1/dashboards/*');
|
||||
|
||||
await page.getByTestId('inline-save-btn').click();
|
||||
|
||||
await saveResponse;
|
||||
|
||||
expect(
|
||||
page.getByLabel('Custom Properties').getByTestId('value')
|
||||
).toContainText(propertyValue);
|
||||
});
|
||||
|
||||
await test.step(
|
||||
'Filter Dashboard using AdvanceSearch Custom Property',
|
||||
async () => {
|
||||
await redirectToHomePage(page);
|
||||
|
||||
const responseExplorePage = page.waitForResponse(
|
||||
'/api/v1/metadata/types/name/storedProcedure?fields=customProperties'
|
||||
);
|
||||
|
||||
await sidebarClick(page, SidebarItem.EXPLORE);
|
||||
|
||||
await responseExplorePage;
|
||||
|
||||
const responseCustomPropertyDashboard = page.waitForResponse(
|
||||
'/api/v1/metadata/types/name/dashboard?fields=customProperties'
|
||||
);
|
||||
|
||||
await page.getByTestId('explore-tree-title-Dashboards').click();
|
||||
|
||||
await responseCustomPropertyDashboard;
|
||||
|
||||
await page.getByTestId('advance-search-button').click();
|
||||
|
||||
await page.waitForSelector('[role="dialog"].ant-modal');
|
||||
|
||||
await expect(page.locator('[role="dialog"].ant-modal')).toBeVisible();
|
||||
|
||||
await expect(page.locator('.ant-modal-title')).toContainText(
|
||||
'Advanced Search'
|
||||
);
|
||||
|
||||
// Select Custom Property Filter
|
||||
|
||||
await page
|
||||
.getByTestId('advanced-search-modal')
|
||||
.getByText('Owner')
|
||||
.click();
|
||||
|
||||
await page.getByTitle('Custom Properties').click();
|
||||
|
||||
// Select Custom Property Field when we want filter
|
||||
await page
|
||||
.locator(
|
||||
'.group--children .rule--field .ant-select-selector .ant-select-selection-search'
|
||||
)
|
||||
.click();
|
||||
await page.getByTitle(propertyName).click();
|
||||
|
||||
// type custom property value based, on which the filter should be made on dashboard
|
||||
await page
|
||||
.locator('.group--children .rule--widget .ant-input')
|
||||
.fill(propertyValue);
|
||||
|
||||
const applyAdvanceFilter = page.waitForRequest('/api/v1/search/query?*');
|
||||
|
||||
await page.getByTestId('apply-btn').click();
|
||||
|
||||
await applyAdvanceFilter;
|
||||
|
||||
// Validate if filter dashboard appeared
|
||||
|
||||
expect(page.getByTestId('advance-search-filter-text')).toContainText(
|
||||
`extension.${propertyName} = '${propertyValue}'`
|
||||
);
|
||||
|
||||
expect(page.getByTestId('entity-header-display-name')).toContainText(
|
||||
dashboardEntity.entity.displayName
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
await test.step('Delete Custom Property ', async () => {
|
||||
await settingClick(page, GlobalSettingOptions.DASHBOARDS, true);
|
||||
await deleteCreatedProperty(page, propertyName);
|
||||
});
|
||||
});
|
@ -29,6 +29,7 @@ import {
|
||||
} from 'react-awesome-query-builder';
|
||||
import { useHistory, useLocation, useParams } from 'react-router-dom';
|
||||
import { emptyJsonTree } from '../../../constants/AdvancedSearch.constants';
|
||||
import { EntityType } from '../../../enums/entity.enum';
|
||||
import { SearchIndex } from '../../../enums/search.enum';
|
||||
import { getTypeByFQN } from '../../../rest/metadataTypeAPI';
|
||||
import advancedSearchClassBase from '../../../utils/AdvancedSearchClassBase';
|
||||
@ -202,6 +203,27 @@ export const AdvanceSearchProvider = ({
|
||||
});
|
||||
}, [history, location.pathname]);
|
||||
|
||||
const fetchCustomPropertyType = async (entityType: EntityType) => {
|
||||
const subfields: Record<
|
||||
string,
|
||||
{ type: string; valueSources: ValueSource[] }
|
||||
> = {};
|
||||
|
||||
const res = await getTypeByFQN(entityType);
|
||||
const customAttributes = res.customProperties;
|
||||
|
||||
if (customAttributes) {
|
||||
customAttributes.forEach((attr) => {
|
||||
subfields[attr.name] = {
|
||||
type: 'text',
|
||||
valueSources: ['value'],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return subfields;
|
||||
};
|
||||
|
||||
async function getCustomAttributesSubfields() {
|
||||
const subfields: Record<
|
||||
string,
|
||||
@ -209,34 +231,39 @@ export const AdvanceSearchProvider = ({
|
||||
> = {};
|
||||
|
||||
try {
|
||||
if (
|
||||
!EntitiesSupportedCustomProperties.includes(
|
||||
isArray(searchIndex) ? searchIndex[0] : searchIndex
|
||||
)
|
||||
) {
|
||||
if (isArray(searchIndex)) {
|
||||
for await (const index of searchIndex) {
|
||||
if (!EntitiesSupportedCustomProperties.includes(index)) {
|
||||
continue; // Skip if entity type does not support custom properties
|
||||
}
|
||||
|
||||
const entityType = getEntityTypeFromSearchIndex(index);
|
||||
|
||||
if (!entityType) {
|
||||
continue; // Skip if entity type is not found
|
||||
}
|
||||
|
||||
try {
|
||||
const propertyTypes = await fetchCustomPropertyType(entityType);
|
||||
Object.assign(subfields, propertyTypes); // Merge the subfields after each API call
|
||||
} catch (error) {
|
||||
continue; // continue the loop if error occurs in one API call
|
||||
}
|
||||
}
|
||||
|
||||
return subfields;
|
||||
} else {
|
||||
if (!EntitiesSupportedCustomProperties.includes(searchIndex)) {
|
||||
return subfields;
|
||||
}
|
||||
|
||||
const entityType = getEntityTypeFromSearchIndex(searchIndex);
|
||||
if (!entityType) {
|
||||
return subfields;
|
||||
}
|
||||
|
||||
return await fetchCustomPropertyType(entityType);
|
||||
}
|
||||
|
||||
const entityType = getEntityTypeFromSearchIndex(
|
||||
isArray(searchIndex) ? searchIndex[0] : searchIndex
|
||||
);
|
||||
if (!entityType) {
|
||||
return subfields;
|
||||
}
|
||||
|
||||
const res = await getTypeByFQN(entityType);
|
||||
const customAttributes = res.customProperties;
|
||||
|
||||
if (customAttributes) {
|
||||
customAttributes.forEach((attr) => {
|
||||
subfields[attr.name] = {
|
||||
type: 'text',
|
||||
valueSources: ['value'],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return subfields;
|
||||
} catch (error) {
|
||||
// Error
|
||||
return subfields;
|
||||
|
@ -36,10 +36,12 @@ import {
|
||||
} from '../../../utils/ExploreUtils';
|
||||
import searchClassBase from '../../../utils/SearchClassBase';
|
||||
|
||||
import { EXPLORE_ROOT_INDEX_MAPPING } from '../../../constants/AdvancedSearch.constants';
|
||||
import serviceUtilClassBase from '../../../utils/ServiceUtilClassBase';
|
||||
import { generateUUID } from '../../../utils/StringsUtils';
|
||||
import { showErrorToast } from '../../../utils/ToastUtils';
|
||||
import { UrlParams } from '../ExplorePage.interface';
|
||||
import { useAdvanceSearch } from '../AdvanceSearchProvider/AdvanceSearchProvider.component';
|
||||
import { ExploreSearchIndex, UrlParams } from '../ExplorePage.interface';
|
||||
import './explore-tree.less';
|
||||
import {
|
||||
ExploreTreeNode,
|
||||
@ -64,6 +66,7 @@ const ExploreTreeTitle = ({ node }: { node: ExploreTreeNode }) => (
|
||||
|
||||
const ExploreTree = ({ onFieldValueSelect }: ExploreTreeProps) => {
|
||||
const { tab } = useParams<UrlParams>();
|
||||
const { onChangeSearchIndex } = useAdvanceSearch();
|
||||
const initTreeData = searchClassBase.getExploreTree();
|
||||
const staticKeysHavingCounts = searchClassBase.staticKeysHavingCounts();
|
||||
const [treeData, setTreeData] = useState(initTreeData);
|
||||
@ -89,9 +92,35 @@ const ExploreTree = ({ onFieldValueSelect }: ExploreTreeProps) => {
|
||||
return [searchQueryParam, defaultServiceType];
|
||||
}, [location.search]);
|
||||
|
||||
const handleChangeSearchIndex = (
|
||||
key: string,
|
||||
rootIndex = SearchIndex.DATABASE,
|
||||
isRoot = false
|
||||
) => {
|
||||
if (isRoot) {
|
||||
onChangeSearchIndex(
|
||||
EXPLORE_ROOT_INDEX_MAPPING[
|
||||
key as keyof typeof EXPLORE_ROOT_INDEX_MAPPING
|
||||
] ?? (key as ExploreSearchIndex)
|
||||
);
|
||||
} else {
|
||||
onChangeSearchIndex(
|
||||
EXPLORE_ROOT_INDEX_MAPPING[
|
||||
rootIndex as keyof typeof EXPLORE_ROOT_INDEX_MAPPING
|
||||
] ?? rootIndex
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const onLoadData = useCallback(
|
||||
async (treeNode: ExploreTreeNode) => {
|
||||
try {
|
||||
handleChangeSearchIndex(
|
||||
treeNode.key,
|
||||
treeNode.data?.rootIndex as SearchIndex,
|
||||
treeNode.data?.isRoot
|
||||
);
|
||||
|
||||
if (treeNode.children) {
|
||||
return;
|
||||
}
|
||||
@ -225,6 +254,13 @@ const ExploreTree = ({ onFieldValueSelect }: ExploreTreeProps) => {
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
handleChangeSearchIndex(
|
||||
node.key,
|
||||
node.data?.rootIndex as SearchIndex,
|
||||
node.data?.isRoot
|
||||
);
|
||||
|
||||
setSelectedKeys([node.key]);
|
||||
},
|
||||
[onFieldValueSelect]
|
||||
|
@ -14,6 +14,7 @@
|
||||
import { t } from 'i18next';
|
||||
import { JsonTree, Utils as QbUtils } from 'react-awesome-query-builder';
|
||||
import { EntityFields } from '../enums/AdvancedSearch.enum';
|
||||
import { SearchIndex } from '../enums/search.enum';
|
||||
|
||||
export const COMMON_DROPDOWN_ITEMS = [
|
||||
{
|
||||
@ -320,3 +321,17 @@ export const MISC_FIELDS = ['owner.displayName', 'tags.tagFQN'];
|
||||
export const OWNER_QUICK_FILTER_DEFAULT_OPTIONS_KEY = 'displayName.keyword';
|
||||
|
||||
export const NULL_OPTION_KEY = 'OM_NULL_FIELD';
|
||||
|
||||
export const EXPLORE_ROOT_INDEX_MAPPING = {
|
||||
[SearchIndex.DATABASE]: [
|
||||
SearchIndex.DATABASE,
|
||||
SearchIndex.DATABASE_SCHEMA,
|
||||
SearchIndex.TABLE,
|
||||
SearchIndex.STORED_PROCEDURE,
|
||||
],
|
||||
[SearchIndex.API_ENDPOINT_INDEX]: [
|
||||
SearchIndex.API_ENDPOINT_INDEX,
|
||||
SearchIndex.API_COLLECTION_INDEX,
|
||||
],
|
||||
Governance: [SearchIndex.GLOSSARY_TERM],
|
||||
};
|
||||
|
@ -83,6 +83,8 @@ export const EntitiesSupportedCustomProperties: string[] = [
|
||||
SearchIndex.TOPIC,
|
||||
SearchIndex.CONTAINER,
|
||||
SearchIndex.MLMODEL,
|
||||
SearchIndex.API_ENDPOINT_INDEX,
|
||||
SearchIndex.API_COLLECTION_INDEX,
|
||||
SearchIndex.SEARCH_INDEX,
|
||||
SearchIndex.GLOSSARY_TERM,
|
||||
];
|
||||
|
Loading…
x
Reference in New Issue
Block a user