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';
|
} from 'react-awesome-query-builder';
|
||||||
import { useHistory, useLocation, useParams } from 'react-router-dom';
|
import { useHistory, useLocation, useParams } from 'react-router-dom';
|
||||||
import { emptyJsonTree } from '../../../constants/AdvancedSearch.constants';
|
import { emptyJsonTree } from '../../../constants/AdvancedSearch.constants';
|
||||||
|
import { EntityType } from '../../../enums/entity.enum';
|
||||||
import { SearchIndex } from '../../../enums/search.enum';
|
import { SearchIndex } from '../../../enums/search.enum';
|
||||||
import { getTypeByFQN } from '../../../rest/metadataTypeAPI';
|
import { getTypeByFQN } from '../../../rest/metadataTypeAPI';
|
||||||
import advancedSearchClassBase from '../../../utils/AdvancedSearchClassBase';
|
import advancedSearchClassBase from '../../../utils/AdvancedSearchClassBase';
|
||||||
@ -202,6 +203,27 @@ export const AdvanceSearchProvider = ({
|
|||||||
});
|
});
|
||||||
}, [history, location.pathname]);
|
}, [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() {
|
async function getCustomAttributesSubfields() {
|
||||||
const subfields: Record<
|
const subfields: Record<
|
||||||
string,
|
string,
|
||||||
@ -209,34 +231,39 @@ export const AdvanceSearchProvider = ({
|
|||||||
> = {};
|
> = {};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (
|
if (isArray(searchIndex)) {
|
||||||
!EntitiesSupportedCustomProperties.includes(
|
for await (const index of searchIndex) {
|
||||||
isArray(searchIndex) ? searchIndex[0] : 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;
|
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) {
|
} catch (error) {
|
||||||
// Error
|
// Error
|
||||||
return subfields;
|
return subfields;
|
||||||
|
@ -36,10 +36,12 @@ import {
|
|||||||
} from '../../../utils/ExploreUtils';
|
} from '../../../utils/ExploreUtils';
|
||||||
import searchClassBase from '../../../utils/SearchClassBase';
|
import searchClassBase from '../../../utils/SearchClassBase';
|
||||||
|
|
||||||
|
import { EXPLORE_ROOT_INDEX_MAPPING } from '../../../constants/AdvancedSearch.constants';
|
||||||
import serviceUtilClassBase from '../../../utils/ServiceUtilClassBase';
|
import serviceUtilClassBase from '../../../utils/ServiceUtilClassBase';
|
||||||
import { generateUUID } from '../../../utils/StringsUtils';
|
import { generateUUID } from '../../../utils/StringsUtils';
|
||||||
import { showErrorToast } from '../../../utils/ToastUtils';
|
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 './explore-tree.less';
|
||||||
import {
|
import {
|
||||||
ExploreTreeNode,
|
ExploreTreeNode,
|
||||||
@ -64,6 +66,7 @@ const ExploreTreeTitle = ({ node }: { node: ExploreTreeNode }) => (
|
|||||||
|
|
||||||
const ExploreTree = ({ onFieldValueSelect }: ExploreTreeProps) => {
|
const ExploreTree = ({ onFieldValueSelect }: ExploreTreeProps) => {
|
||||||
const { tab } = useParams<UrlParams>();
|
const { tab } = useParams<UrlParams>();
|
||||||
|
const { onChangeSearchIndex } = useAdvanceSearch();
|
||||||
const initTreeData = searchClassBase.getExploreTree();
|
const initTreeData = searchClassBase.getExploreTree();
|
||||||
const staticKeysHavingCounts = searchClassBase.staticKeysHavingCounts();
|
const staticKeysHavingCounts = searchClassBase.staticKeysHavingCounts();
|
||||||
const [treeData, setTreeData] = useState(initTreeData);
|
const [treeData, setTreeData] = useState(initTreeData);
|
||||||
@ -89,9 +92,35 @@ const ExploreTree = ({ onFieldValueSelect }: ExploreTreeProps) => {
|
|||||||
return [searchQueryParam, defaultServiceType];
|
return [searchQueryParam, defaultServiceType];
|
||||||
}, [location.search]);
|
}, [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(
|
const onLoadData = useCallback(
|
||||||
async (treeNode: ExploreTreeNode) => {
|
async (treeNode: ExploreTreeNode) => {
|
||||||
try {
|
try {
|
||||||
|
handleChangeSearchIndex(
|
||||||
|
treeNode.key,
|
||||||
|
treeNode.data?.rootIndex as SearchIndex,
|
||||||
|
treeNode.data?.isRoot
|
||||||
|
);
|
||||||
|
|
||||||
if (treeNode.children) {
|
if (treeNode.children) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -225,6 +254,13 @@ const ExploreTree = ({ onFieldValueSelect }: ExploreTreeProps) => {
|
|||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleChangeSearchIndex(
|
||||||
|
node.key,
|
||||||
|
node.data?.rootIndex as SearchIndex,
|
||||||
|
node.data?.isRoot
|
||||||
|
);
|
||||||
|
|
||||||
setSelectedKeys([node.key]);
|
setSelectedKeys([node.key]);
|
||||||
},
|
},
|
||||||
[onFieldValueSelect]
|
[onFieldValueSelect]
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
import { t } from 'i18next';
|
import { t } from 'i18next';
|
||||||
import { JsonTree, Utils as QbUtils } from 'react-awesome-query-builder';
|
import { JsonTree, Utils as QbUtils } from 'react-awesome-query-builder';
|
||||||
import { EntityFields } from '../enums/AdvancedSearch.enum';
|
import { EntityFields } from '../enums/AdvancedSearch.enum';
|
||||||
|
import { SearchIndex } from '../enums/search.enum';
|
||||||
|
|
||||||
export const COMMON_DROPDOWN_ITEMS = [
|
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 OWNER_QUICK_FILTER_DEFAULT_OPTIONS_KEY = 'displayName.keyword';
|
||||||
|
|
||||||
export const NULL_OPTION_KEY = 'OM_NULL_FIELD';
|
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.TOPIC,
|
||||||
SearchIndex.CONTAINER,
|
SearchIndex.CONTAINER,
|
||||||
SearchIndex.MLMODEL,
|
SearchIndex.MLMODEL,
|
||||||
|
SearchIndex.API_ENDPOINT_INDEX,
|
||||||
|
SearchIndex.API_COLLECTION_INDEX,
|
||||||
SearchIndex.SEARCH_INDEX,
|
SearchIndex.SEARCH_INDEX,
|
||||||
SearchIndex.GLOSSARY_TERM,
|
SearchIndex.GLOSSARY_TERM,
|
||||||
];
|
];
|
||||||
|
Loading…
x
Reference in New Issue
Block a user