Data products explore tab (#13578)

* fix: add data products in explore tab

* fix: add data products to explore

* fix: update icon positioning

* fix: make dataProducts as default tab on explore

* fix: multiple api calls

* fix: data product feedback

* fix: filtering ui screen for data product

* fix: sonar tests

---------

Co-authored-by: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com>
This commit is contained in:
karanh37 2023-10-17 15:10:28 +05:30 committed by GitHub
parent c65518ac04
commit 10ef83779e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 248 additions and 116 deletions

View File

@ -0,0 +1,3 @@
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M2 4.66667C2 4.29848 2.29848 4 2.66667 4H13.3333C13.7015 4 14 4.29848 14 4.66667C14 5.03485 13.7015 5.33333 13.3333 5.33333H2.66667C2.29848 5.33333 2 5.03485 2 4.66667ZM4 8C4 7.6318 4.29848 7.33333 4.66667 7.33333H11.3333C11.7015 7.33333 12 7.6318 12 8C12 8.3682 11.7015 8.66667 11.3333 8.66667H4.66667C4.29848 8.66667 4 8.3682 4 8ZM6 11.3333C6 10.9651 6.29848 10.6667 6.66667 10.6667H9.33333C9.70153 10.6667 10 10.9651 10 11.3333C10 11.7015 9.70153 12 9.33333 12H6.66667C6.29848 12 6 11.7015 6 11.3333Z" fill="#292929"/>
</svg>

After

Width:  |  Height:  |  Size: 651 B

View File

@ -240,6 +240,9 @@ const Appbar: React.FC = (): JSX.Element => {
tab: defaultTab, tab: defaultTab,
search: value, search: value,
isPersistFilters: false, isPersistFilters: false,
extraParameters: {
sort: '_score',
},
}) })
); );
} }

View File

@ -38,6 +38,7 @@ export interface AssetSelectionModalProps {
type?: AssetsOfEntity; type?: AssetsOfEntity;
onCancel: () => void; onCancel: () => void;
onSave?: () => void; onSave?: () => void;
queryFilter?: Record<string, unknown>;
} }
export type AssetsUnion = export type AssetsUnion =

View File

@ -38,7 +38,6 @@ import {
getAssetsSearchIndex, getAssetsSearchIndex,
getEntityAPIfromSource, getEntityAPIfromSource,
} from '../../../utils/Assets/AssetsUtils'; } from '../../../utils/Assets/AssetsUtils';
import { getQueryFilterToExcludeTerm } from '../../../utils/GlossaryUtils';
import Searchbar from '../../common/searchbar/Searchbar'; import Searchbar from '../../common/searchbar/Searchbar';
import TableDataCardV2 from '../../common/table-data-card-v2/TableDataCardV2'; import TableDataCardV2 from '../../common/table-data-card-v2/TableDataCardV2';
import { AssetsOfEntity } from '../../Glossary/GlossaryTerms/tabs/AssetsTabs.interface'; import { AssetsOfEntity } from '../../Glossary/GlossaryTerms/tabs/AssetsTabs.interface';
@ -53,6 +52,7 @@ export const AssetSelectionModal = ({
onSave, onSave,
open, open,
type = AssetsOfEntity.GLOSSARY, type = AssetsOfEntity.GLOSSARY,
queryFilter = {},
}: AssetSelectionModalProps) => { }: AssetSelectionModalProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
@ -67,14 +67,6 @@ export const AssetSelectionModal = ({
const [pageNumber, setPageNumber] = useState(1); const [pageNumber, setPageNumber] = useState(1);
const [totalCount, setTotalCount] = useState(0); const [totalCount, setTotalCount] = useState(0);
const queryFilter = useMemo(() => {
if (type === AssetsOfEntity.GLOSSARY) {
return getQueryFilterToExcludeTerm(entityFqn);
} else {
return {};
}
}, [entityFqn, type]);
const fetchEntities = useCallback( const fetchEntities = useCallback(
async ({ searchText = '', page = 1, index = activeFilter }) => { async ({ searchText = '', page = 1, index = activeFilter }) => {
try { try {
@ -106,7 +98,7 @@ export const AssetSelectionModal = ({
} else if (type === AssetsOfEntity.DATA_PRODUCT) { } else if (type === AssetsOfEntity.DATA_PRODUCT) {
const data = await getDataProductByName( const data = await getDataProductByName(
encodeURIComponent(entityFqn), encodeURIComponent(entityFqn),
'' 'domain'
); );
setActiveEntity(data); setActiveEntity(data);
} }

View File

@ -44,10 +44,7 @@ import { EntityDetailsObjectInterface } from '../../../components/Explore/explor
import AssetsTabs, { import AssetsTabs, {
AssetsTabRef, AssetsTabRef,
} from '../../../components/Glossary/GlossaryTerms/tabs/AssetsTabs.component'; } from '../../../components/Glossary/GlossaryTerms/tabs/AssetsTabs.component';
import { import { AssetsOfEntity } from '../../../components/Glossary/GlossaryTerms/tabs/AssetsTabs.interface';
AssetsOfEntity,
AssetsViewType,
} from '../../../components/Glossary/GlossaryTerms/tabs/AssetsTabs.interface';
import EntityDeleteModal from '../../../components/Modals/EntityDeleteModal/EntityDeleteModal'; import EntityDeleteModal from '../../../components/Modals/EntityDeleteModal/EntityDeleteModal';
import EntityNameModal from '../../../components/Modals/EntityNameModal/EntityNameModal.component'; import EntityNameModal from '../../../components/Modals/EntityNameModal/EntityNameModal.component';
import { usePermissionProvider } from '../../../components/PermissionProvider/PermissionProvider'; import { usePermissionProvider } from '../../../components/PermissionProvider/PermissionProvider';
@ -56,7 +53,6 @@ import {
ResourceEntity, ResourceEntity,
} from '../../../components/PermissionProvider/PermissionProvider.interface'; } from '../../../components/PermissionProvider/PermissionProvider.interface';
import TabsLabel from '../../../components/TabsLabel/TabsLabel.component'; import TabsLabel from '../../../components/TabsLabel/TabsLabel.component';
import { FQN_SEPARATOR_CHAR } from '../../../constants/char.constants';
import { DE_ACTIVE_COLOR } from '../../../constants/constants'; import { DE_ACTIVE_COLOR } from '../../../constants/constants';
import { EntityField } from '../../../constants/Feeds.constants'; import { EntityField } from '../../../constants/Feeds.constants';
import { myDataSearchIndex } from '../../../constants/Mydata.constants'; import { myDataSearchIndex } from '../../../constants/Mydata.constants';
@ -70,8 +66,9 @@ import { Operation } from '../../../generated/entity/policies/policy';
import { Style } from '../../../generated/type/tagLabel'; import { Style } from '../../../generated/type/tagLabel';
import { searchData } from '../../../rest/miscAPI'; import { searchData } from '../../../rest/miscAPI';
import { getEntityDeleteMessage } from '../../../utils/CommonUtils'; import { getEntityDeleteMessage } from '../../../utils/CommonUtils';
import { getQueryFilterToIncludeDomain } from '../../../utils/DomainUtils';
import { getEntityName } from '../../../utils/EntityUtils';
import { getEntityVersionByField } from '../../../utils/EntityVersionUtils'; import { getEntityVersionByField } from '../../../utils/EntityVersionUtils';
import Fqn from '../../../utils/Fqn.js';
import { import {
checkPermission, checkPermission,
DEFAULT_ENTITY_PERMISSION, DEFAULT_ENTITY_PERMISSION,
@ -117,25 +114,18 @@ const DataProductsDetailsPage = ({
const [assetCount, setAssetCount] = useState<number>(0); const [assetCount, setAssetCount] = useState<number>(0);
const breadcrumbs = useMemo(() => { const breadcrumbs = useMemo(() => {
if (!dataProductFqn) { if (!dataProduct.domain) {
return []; return [];
} }
const arr = Fqn.split(dataProductFqn);
const dataFQN: Array<string> = [];
return [ return [
...arr.slice(0, -1).map((d) => { {
dataFQN.push(d); name: getEntityName(dataProduct.domain),
url: getDomainPath(dataProduct.domain.fullyQualifiedName),
return {
name: d,
url: getDomainPath(dataFQN.join(FQN_SEPARATOR_CHAR)),
activeTitle: false, activeTitle: false,
}; },
}),
]; ];
}, [dataProductFqn]); }, [dataProduct.domain]);
const [name, displayName] = useMemo(() => { const [name, displayName] = useMemo(() => {
const defaultName = dataProduct.name; const defaultName = dataProduct.name;
@ -425,7 +415,6 @@ const DataProductsDetailsPage = ({
permissions={dataProductPermission} permissions={dataProductPermission}
ref={assetTabRef} ref={assetTabRef}
type={AssetsOfEntity.DATA_PRODUCT} type={AssetsOfEntity.DATA_PRODUCT}
viewType={AssetsViewType.TABS}
onAddAsset={() => setAssetModelVisible(true)} onAddAsset={() => setAssetModelVisible(true)}
onAssetClick={handleAssetClick} onAssetClick={handleAssetClick}
/> />
@ -575,6 +564,9 @@ const DataProductsDetailsPage = ({
<AssetSelectionModal <AssetSelectionModal
entityFqn={dataProductFqn} entityFqn={dataProductFqn}
open={assetModalVisible} open={assetModalVisible}
queryFilter={getQueryFilterToIncludeDomain(
dataProduct.domain?.fullyQualifiedName ?? ''
)}
type={AssetsOfEntity.DATA_PRODUCT} type={AssetsOfEntity.DATA_PRODUCT}
onCancel={() => setAssetModelVisible(false)} onCancel={() => setAssetModelVisible(false)}
onSave={handleAssetSave} onSave={handleAssetSave}

View File

@ -40,6 +40,5 @@
} }
.summary-panel-container { .summary-panel-container {
height: @domain-page-height; height: @domain-page-height;
border-left: 1px solid @border-color;
} }
} }

View File

@ -101,7 +101,7 @@ const DataProductsPage = () => {
try { try {
const data = await getDataProductByName( const data = await getDataProductByName(
encodeURIComponent(fqn), encodeURIComponent(fqn),
'owner,experts' 'domain,owner,experts'
); );
setDataProduct(data); setDataProduct(data);

View File

@ -11,7 +11,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { isNil, isString } from 'lodash'; import { cloneDeep, isNil, isString } from 'lodash';
import Qs from 'qs'; import Qs from 'qs';
import React, { import React, {
useCallback, useCallback,
@ -22,9 +22,11 @@ import React, {
} from 'react'; } from 'react';
import { import {
Config, Config,
FieldGroup,
ImmutableTree, ImmutableTree,
JsonTree, JsonTree,
Utils as QbUtils, Utils as QbUtils,
ValueSource,
} 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 { import {
@ -33,7 +35,9 @@ import {
} from '../../../constants/AdvancedSearch.constants'; } from '../../../constants/AdvancedSearch.constants';
import { tabsInfo } from '../../../constants/explore.constants'; import { tabsInfo } from '../../../constants/explore.constants';
import { SearchIndex } from '../../../enums/search.enum'; import { SearchIndex } from '../../../enums/search.enum';
import { getTypeByFQN } from '../../../rest/metadataTypeAPI';
import { elasticSearchFormat } from '../../../utils/QueryBuilderElasticsearchFormatUtils'; import { elasticSearchFormat } from '../../../utils/QueryBuilderElasticsearchFormatUtils';
import { getEntityTypeFromSearchIndex } from '../../../utils/SearchUtils';
import Loader from '../../Loader/Loader'; import Loader from '../../Loader/Loader';
import { AdvancedSearchModal } from '../AdvanceSearchModal.component'; import { AdvancedSearchModal } from '../AdvanceSearchModal.component';
import { ExploreSearchIndex, UrlParams } from '../explore.interface'; import { ExploreSearchIndex, UrlParams } from '../explore.interface';
@ -165,15 +169,55 @@ export const AdvanceSearchProvider = ({
}); });
}, [history, location.pathname]); }, [history, location.pathname]);
useEffect(() => { async function getCustomAttributesSubfields() {
if (jsonTree) { const updatedConfig = cloneDeep(config);
const tree = QbUtils.checkTree(QbUtils.loadTree(jsonTree), config); try {
const entityType = getEntityTypeFromSearchIndex(searchIndex);
if (!entityType) {
return;
}
const res = await getTypeByFQN(entityType);
const customAttributes = res.customProperties;
const subfields: Record<
string,
{ type: string; valueSources: ValueSource[] }
> = {};
if (customAttributes) {
customAttributes.forEach((attr) => {
subfields[attr.name] = {
type: 'text',
valueSources: ['value'],
};
});
}
(updatedConfig.fields.extension as FieldGroup).subfields = subfields;
return updatedConfig;
} catch (error) {
// Error
return updatedConfig;
}
}
const loadTree = useCallback(
async (treeObj: JsonTree) => {
const updatedConfig = (await getCustomAttributesSubfields()) ?? config;
const tree = QbUtils.checkTree(QbUtils.loadTree(treeObj), updatedConfig);
setTreeInternal(tree); setTreeInternal(tree);
const qFilter = { const qFilter = {
query: elasticSearchFormat(tree, config), query: elasticSearchFormat(tree, updatedConfig),
}; };
setQueryFilter(qFilter); setQueryFilter(qFilter);
setSQLQuery(QbUtils.sqlFormat(tree, config) ?? ''); setSQLQuery(QbUtils.sqlFormat(tree, updatedConfig) ?? '');
},
[config]
);
useEffect(() => {
if (jsonTree) {
loadTree(jsonTree);
} else { } else {
handleReset(); handleReset();
} }

View File

@ -16,6 +16,7 @@ import { useTranslation } from 'react-i18next';
import { DataProduct } from '../../../../generated/entity/domains/dataProduct'; import { DataProduct } from '../../../../generated/entity/domains/dataProduct';
import { getEntityName } from '../../../../utils/EntityUtils'; import { getEntityName } from '../../../../utils/EntityUtils';
import { OwnerLabel } from '../../../common/OwnerLabel/OwnerLabel.component'; import { OwnerLabel } from '../../../common/OwnerLabel/OwnerLabel.component';
import RichTextEditorPreviewer from '../../../common/rich-text-editor/RichTextEditorPreviewer';
import SummaryPanelSkeleton from '../../../Skeleton/SummaryPanelSkeleton/SummaryPanelSkeleton.component'; import SummaryPanelSkeleton from '../../../Skeleton/SummaryPanelSkeleton/SummaryPanelSkeleton.component';
interface DataProductSummaryProps { interface DataProductSummaryProps {
@ -50,6 +51,32 @@ const DataProductSummary = ({
</Row> </Row>
<Divider className="m-y-xs" /> <Divider className="m-y-xs" />
<Row className="m-md" gutter={[0, 8]}>
<Col span={24}>
<Typography.Text
className="summary-panel-section-title"
data-testid="description-header">
{t('label.description')}
</Typography.Text>
</Col>
<Col span={24}>
<div>
{entityDetails.description?.trim() ? (
<RichTextEditorPreviewer
markdown={entityDetails.description}
maxLength={80}
/>
) : (
<Typography className="text-grey-body">
{t('label.no-data-found')}
</Typography>
)}
</div>
</Col>
</Row>
<Divider className="m-y-xs" />
<Row className="m-md m-t-0" gutter={[0, 8]}> <Row className="m-md m-t-0" gutter={[0, 8]}>
<Col span={24}> <Col span={24}>
<Typography.Text <Typography.Text

View File

@ -268,6 +268,11 @@ export const MOCK_EXPLORE_SEARCH_RESULTS: SearchResponse<ExploreSearchIndex> = {
}; };
export const MOCK_EXPLORE_TAB_ITEMS = [ export const MOCK_EXPLORE_TAB_ITEMS = [
{
key: 'data_product_search_index',
label: 'data_product_search_index',
count: 0,
},
{ {
key: 'table_search_index', key: 'table_search_index',
label: 'table_search_index', label: 'table_search_index',

View File

@ -46,6 +46,7 @@ export type UrlParams = {
}; };
export type ExploreSearchIndex = export type ExploreSearchIndex =
| SearchIndex.DATA_PRODUCT
| SearchIndex.TABLE | SearchIndex.TABLE
| SearchIndex.PIPELINE | SearchIndex.PIPELINE
| SearchIndex.DASHBOARD | SearchIndex.DASHBOARD

View File

@ -101,7 +101,8 @@ const ExploreSearchCard: React.FC<ExploreSearchCardProps> = forwardRef<
if ( if (
source.entityType !== EntityType.GLOSSARY_TERM && source.entityType !== EntityType.GLOSSARY_TERM &&
source.entityType !== EntityType.TAG source.entityType !== EntityType.TAG &&
source.entityType !== EntityType.DATA_PRODUCT
) { ) {
_otherDetails.push({ _otherDetails.push({
key: 'Tier', key: 'Tier',

View File

@ -76,7 +76,6 @@ const ExploreV1: React.FC<ExploreProps> = ({
quickFilters, quickFilters,
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
// const { tab } = useParams<{ tab: string }>();
const [selectedQuickFilters, setSelectedQuickFilters] = useState< const [selectedQuickFilters, setSelectedQuickFilters] = useState<
ExploreQuickFilterField[] ExploreQuickFilterField[]
>([] as ExploreQuickFilterField[]); >([] as ExploreQuickFilterField[]);

View File

@ -67,6 +67,7 @@ const props = {
tabItems: MOCK_EXPLORE_TAB_ITEMS, tabItems: MOCK_EXPLORE_TAB_ITEMS,
activeTabKey: SearchIndex.TABLE, activeTabKey: SearchIndex.TABLE,
tabCounts: { tabCounts: {
data_product_search_index: 0,
table_search_index: 20, table_search_index: 20,
topic_search_index: 10, topic_search_index: 10,
dashboard_search_index: 14, dashboard_search_index: 14,

View File

@ -101,10 +101,15 @@ export const FeedEditor = forwardRef<editorRef, FeedEditorProp>(
renderLoading: () => `${t('label.loading')}...`, renderLoading: () => `${t('label.loading')}...`,
renderItem: (item: Record<string, any>) => { renderItem: (item: Record<string, any>) => {
if (!item.type) { if (!item.type) {
return `<div class="d-flex gap-2"> const userResult = `<div class="d-flex gap-2">
${item.avatarEle} ${item.avatarEle}
<span class="d-flex items-center truncate w-56">${item.name}</span> <span class="d-flex items-center truncate w-56">${item.name}</span>
</div>`; </div>`;
const userWrapper = document.createElement('div');
userWrapper.innerHTML = userResult;
return userWrapper;
} }
const breadcrumbsData = item.breadcrumbs const breadcrumbsData = item.breadcrumbs
@ -127,7 +132,7 @@ export const FeedEditor = forwardRef<editorRef, FeedEditorProp>(
? `<span class="text-grey-muted text-xs">${item.type}</span>` ? `<span class="text-grey-muted text-xs">${item.type}</span>`
: ''; : '';
return `<div class="d-flex items-center gap-2"> const result = `<div class="d-flex items-center gap-2">
<div class="flex-center mention-icon-image">${icon}</div> <div class="flex-center mention-icon-image">${icon}</div>
<div> <div>
${breadcrumbEle} ${breadcrumbEle}
@ -137,6 +142,11 @@ export const FeedEditor = forwardRef<editorRef, FeedEditorProp>(
</div> </div>
</div> </div>
</div>`; </div>`;
const wrapper = document.createElement('div');
wrapper.innerHTML = result;
return wrapper;
}, },
}, },
markdownOptions: {}, markdownOptions: {},

View File

@ -25,6 +25,7 @@ import { ChangeDescription } from '../../../generated/entity/type';
import { searchData } from '../../../rest/miscAPI'; import { searchData } from '../../../rest/miscAPI';
import { getCountBadge, getFeedCounts } from '../../../utils/CommonUtils'; import { getCountBadge, getFeedCounts } from '../../../utils/CommonUtils';
import { getEntityVersionByField } from '../../../utils/EntityVersionUtils'; import { getEntityVersionByField } from '../../../utils/EntityVersionUtils';
import { getQueryFilterToExcludeTerm } from '../../../utils/GlossaryUtils';
import { getGlossaryTermsVersionsPath } from '../../../utils/RouterUtils'; import { getGlossaryTermsVersionsPath } from '../../../utils/RouterUtils';
import { getEncodedFqn } from '../../../utils/StringsUtils'; import { getEncodedFqn } from '../../../utils/StringsUtils';
import { ActivityFeedTab } from '../../ActivityFeed/ActivityFeedTab/ActivityFeedTab.component'; import { ActivityFeedTab } from '../../ActivityFeed/ActivityFeedTab/ActivityFeedTab.component';
@ -321,6 +322,9 @@ const GlossaryTermsV1 = ({
<AssetSelectionModal <AssetSelectionModal
entityFqn={glossaryTerm.fullyQualifiedName} entityFqn={glossaryTerm.fullyQualifiedName}
open={assetModalVisible} open={assetModalVisible}
queryFilter={getQueryFilterToExcludeTerm(
glossaryTerm.fullyQualifiedName
)}
type={AssetsOfEntity.GLOSSARY} type={AssetsOfEntity.GLOSSARY}
onCancel={() => setAssetModelVisible(false)} onCancel={() => setAssetModelVisible(false)}
onSave={handleAssetSave} onSave={handleAssetSave}

View File

@ -12,8 +12,6 @@
*/ */
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import React from 'react'; import React from 'react';
import AppState from '../../AppState';
import { User } from '../../generated/entity/teams/user';
import { import {
getEntityPermissionByFqn, getEntityPermissionByFqn,
getEntityPermissionById, getEntityPermissionById,
@ -37,13 +35,25 @@ jest.mock('../../rest/permissionAPI', () => ({
.mockImplementation(() => Promise.resolve({})), .mockImplementation(() => Promise.resolve({})),
})); }));
jest.mock('react-router-dom', () => ({
useHistory: jest.fn().mockReturnValue({ push: jest.fn(), listen: jest.fn() }),
}));
let currentUser: { id: string; name: string } | null = {
id: '123',
name: 'Test User',
};
jest.mock('../authentication/auth-provider/AuthProvider', () => {
return {
useAuthContext: jest.fn().mockImplementation(() => ({
currentUser,
})),
};
});
describe('PermissionProvider', () => { describe('PermissionProvider', () => {
it('Should render children and call apis when current user is present', async () => { it('Should render children and call apis when current user is present', async () => {
const currentUser = { id: '123', name: 'Test User' };
const getUserDetailsSpy = jest
.spyOn(AppState, 'getCurrentUserDetails')
.mockReturnValue(currentUser as User);
render( render(
<PermissionProvider> <PermissionProvider>
<div data-testid="children">Children</div> <div data-testid="children">Children</div>
@ -57,15 +67,10 @@ describe('PermissionProvider', () => {
expect(getResourcePermission).not.toHaveBeenCalled(); expect(getResourcePermission).not.toHaveBeenCalled();
expect(await screen.findByTestId('children')).toBeInTheDocument(); expect(await screen.findByTestId('children')).toBeInTheDocument();
getUserDetailsSpy.mockRestore();
}); });
it('Should not call apis when current user is undefined', async () => { it('Should not call apis when current user is undefined', async () => {
const getUserDetailsSpy = jest currentUser = null;
.spyOn(AppState, 'getCurrentUserDetails')
.mockReturnValue(undefined);
render( render(
<PermissionProvider> <PermissionProvider>
<div data-testid="children">Children</div> <div data-testid="children">Children</div>
@ -77,7 +82,5 @@ describe('PermissionProvider', () => {
expect(getEntityPermissionById).not.toHaveBeenCalled(); expect(getEntityPermissionById).not.toHaveBeenCalled();
expect(getEntityPermissionByFqn).not.toHaveBeenCalled(); expect(getEntityPermissionByFqn).not.toHaveBeenCalled();
expect(getResourcePermission).not.toHaveBeenCalled(); expect(getResourcePermission).not.toHaveBeenCalled();
getUserDetailsSpy.mockRestore();
}); });
}); });

View File

@ -24,7 +24,6 @@ import React, {
useState, useState,
} from 'react'; } from 'react';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import AppState from '../../AppState';
import Loader from '../../components/Loader/Loader'; import Loader from '../../components/Loader/Loader';
import { REDIRECT_PATHNAME } from '../../constants/constants'; import { REDIRECT_PATHNAME } from '../../constants/constants';
import { import {
@ -41,6 +40,7 @@ import {
getOperationPermissions, getOperationPermissions,
getUIPermission, getUIPermission,
} from '../../utils/PermissionsUtils'; } from '../../utils/PermissionsUtils';
import { useAuthContext } from '../authentication/auth-provider/AuthProvider';
import { import {
EntityPermissionMap, EntityPermissionMap,
PermissionContextType, PermissionContextType,
@ -67,6 +67,7 @@ const PermissionProvider: FC<PermissionProviderProps> = ({ children }) => {
const [permissions, setPermissions] = useState<UIPermission>( const [permissions, setPermissions] = useState<UIPermission>(
{} as UIPermission {} as UIPermission
); );
const { currentUser } = useAuthContext();
const cookieStorage = new CookieStorage(); const cookieStorage = new CookieStorage();
const history = useHistory(); const history = useHistory();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@ -78,11 +79,6 @@ const PermissionProvider: FC<PermissionProviderProps> = ({ children }) => {
{} as UIPermission {} as UIPermission
); );
// Update current user details of AppState change
const currentUser = useMemo(() => {
return AppState.getCurrentUserDetails();
}, [AppState.userDetails, AppState.nonSecureUserDetails]);
const redirectToStoredPath = useCallback(() => { const redirectToStoredPath = useCallback(() => {
const urlPathname = cookieStorage.getItem(REDIRECT_PATHNAME); const urlPathname = cookieStorage.getItem(REDIRECT_PATHNAME);
if (urlPathname) { if (urlPathname) {

View File

@ -29,7 +29,7 @@
} }
.ant-tabs-nav { .ant-tabs-nav {
padding: 8px 16px 0 28px; padding: 8px 16px 0 28px;
margin-bottom: 0; margin-bottom: 0 !important;
} }
.widget-manage-dropdown .ant-dropdown-trigger { .widget-manage-dropdown .ant-dropdown-trigger {

View File

@ -84,14 +84,8 @@ const EntitySummaryDetails = ({
/> />
); );
const { const { isEntityDetails, userDetails, isTier, isOwner, isTeamOwner } =
isEntityDetails, useMemo(() => {
userDetails,
isTier,
isOwner,
isTeamOwner,
} = useMemo(() => {
const userDetails = getTeamsUser(data); const userDetails = getTeamsUser(data);
return { return {
@ -100,7 +94,9 @@ const EntitySummaryDetails = ({
userDetails, userDetails,
isTier: data.key === 'Tier', isTier: data.key === 'Tier',
isOwner: data.key === 'Owner', isOwner: data.key === 'Owner',
isTeamOwner: isString(data.value) ? data.value.includes('teams/') : false, isTeamOwner: isString(data.value)
? data.value.includes('teams/')
: false,
}; };
}, [data]); }, [data]);
@ -129,7 +125,11 @@ const EntitySummaryDetails = ({
</> </>
)} )}
{isTeamOwner ? ( {isTeamOwner ? (
<IconTeamsGrey height={18} width={18} /> <IconTeamsGrey
className="align-middle"
height={18}
width={18}
/>
) : ( ) : (
<ProfilePicture <ProfilePicture
displayName={displayVal} displayName={displayVal}

View File

@ -10,11 +10,11 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import { FilterOutlined } from '@ant-design/icons';
import { Button, Checkbox, List, Popover, Space, Typography } from 'antd'; import { Button, Checkbox, List, Popover, Space, Typography } from 'antd';
import React, { useCallback, useEffect, useMemo, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import AppState from '../../../AppState'; import AppState from '../../../AppState';
import { ReactComponent as FilterIcon } from '../../../assets/svg/ic-feeds-filter.svg';
import { FeedFilter } from '../../../enums/mydata.enum'; import { FeedFilter } from '../../../enums/mydata.enum';
import './feeds-filter-popover.less'; import './feeds-filter-popover.less';
import { FeedsFilterPopoverProps } from './FeedsFilterPopover.interface'; import { FeedsFilterPopoverProps } from './FeedsFilterPopover.interface';
@ -126,7 +126,7 @@ const FeedsFilterPopover = ({
showArrow={false} showArrow={false}
trigger="click" trigger="click"
onOpenChange={setPopupVisible}> onOpenChange={setPopupVisible}>
<Button icon={<FilterOutlined />}>{t('label.filter-plural')}</Button> <Button className="flex-center" icon={<FilterIcon height={16} />} />
</Popover> </Popover>
); );
}; };

View File

@ -162,6 +162,17 @@ export const TAG_DROPDOWN_ITEMS = [
}, },
]; ];
export const DATA_PRODUCT_DROPDOWN_ITEMS = [
{
label: t('label.domain'),
key: 'domain.displayName.keyword',
},
{
label: t('label.owner'),
key: 'owner.displayName.keyword',
},
];
export const ALL_DROPDOWN_ITEMS = [ export const ALL_DROPDOWN_ITEMS = [
...COMMON_DROPDOWN_ITEMS, ...COMMON_DROPDOWN_ITEMS,
...TABLE_DROPDOWN_ITEMS, ...TABLE_DROPDOWN_ITEMS,

View File

@ -26,7 +26,7 @@ export const SIDEBAR_LIST = [
{ {
key: ROUTES.EXPLORE, key: ROUTES.EXPLORE,
label: i18next.t('label.explore'), label: i18next.t('label.explore'),
redirect_url: '/explore/tables', redirect_url: '/explore/dataProducts',
icon: ExploreIcon, icon: ExploreIcon,
dataTestId: 'app-bar-item-explore', dataTestId: 'app-bar-item-explore',
}, },

View File

@ -56,6 +56,12 @@ export interface ExploreTabInfo {
} }
export const tabsInfo: { [K in ExploreSearchIndex]: ExploreTabInfo } = { export const tabsInfo: { [K in ExploreSearchIndex]: ExploreTabInfo } = {
[SearchIndex.DATA_PRODUCT]: {
label: i18n.t('label.data-product-plural'),
sortingFields: tableSortingFields,
sortField: INITIAL_SORT_FIELD,
path: 'dataProducts',
},
[SearchIndex.TABLE]: { [SearchIndex.TABLE]: {
label: i18n.t('label.table-plural'), label: i18n.t('label.table-plural'),
sortingFields: tableSortingFields, sortingFields: tableSortingFields,

View File

@ -1917,6 +1917,7 @@ export const mockSearchData = {
}; };
export const MOCK_EXPLORE_PAGE_COUNT = { export const MOCK_EXPLORE_PAGE_COUNT = {
[SearchIndex.DATA_PRODUCT]: 0,
[SearchIndex.TABLE]: mockSearchData.hits.total.value, [SearchIndex.TABLE]: mockSearchData.hits.total.value,
[SearchIndex.TOPIC]: 0, [SearchIndex.TOPIC]: 0,
[SearchIndex.DASHBOARD]: 0, [SearchIndex.DASHBOARD]: 0,

View File

@ -77,8 +77,6 @@ const ExplorePageV1: FunctionComponent = () => {
const [advancesSearchQuickFilters, setAdvancedSearchQuickFilters] = const [advancesSearchQuickFilters, setAdvancedSearchQuickFilters] =
useState<QueryFilterInterface>(); useState<QueryFilterInterface>();
const [sortValue, setSortValue] = useState<string>(INITIAL_SORT_FIELD);
const [sortOrder, setSortOrder] = useState<SORT_ORDER>(SORT_ORDER.DESC); const [sortOrder, setSortOrder] = useState<SORT_ORDER>(SORT_ORDER.DESC);
const [searchHitCounts, setSearchHitCounts] = useState<SearchHitCounts>(); const [searchHitCounts, setSearchHitCounts] = useState<SearchHitCounts>();
@ -87,20 +85,23 @@ const ExplorePageV1: FunctionComponent = () => {
const { queryFilter } = useAdvanceSearch(); const { queryFilter } = useAdvanceSearch();
const parsedSearch = useMemo( const [parsedSearch, searchQueryParam, sortValue] = useMemo(() => {
() => const parsedSearch = Qs.parse(
Qs.parse(
location.search.startsWith('?') location.search.startsWith('?')
? location.search.substring(1) ? location.search.substring(1)
: location.search : location.search
),
[location.search]
); );
const searchQueryParam = useMemo( const searchQueryParam = isString(parsedSearch.search)
() => (isString(parsedSearch.search) ? parsedSearch.search : ''), ? parsedSearch.search
[location.search] : '';
);
const sortValue = isString(parsedSearch.sort)
? parsedSearch.sort
: INITIAL_SORT_FIELD;
return [parsedSearch, searchQueryParam, sortValue];
}, [location.search]);
const handlePageChange: ExploreProps['onChangePage'] = (page, size) => { const handlePageChange: ExploreProps['onChangePage'] = (page, size) => {
history.push({ history.push({
@ -108,6 +109,17 @@ const ExplorePageV1: FunctionComponent = () => {
}); });
}; };
const handleSortValueChange = (page: number, sortVal: string) => {
history.push({
search: Qs.stringify({
...parsedSearch,
page,
size: size ?? PAGE_SIZE,
sort: sortVal,
}),
});
};
// Filters that can be common for all the Entities Ex. Tables, Topics, etc. // Filters that can be common for all the Entities Ex. Tables, Topics, etc.
const commonQuickFilters = useMemo(() => { const commonQuickFilters = useMemo(() => {
const mustField: QueryFieldInterface[] = get( const mustField: QueryFieldInterface[] = get(
@ -152,6 +164,7 @@ const ExplorePageV1: FunctionComponent = () => {
getExplorePath({ getExplorePath({
tab: tabsInfo[nSearchIndex].path, tab: tabsInfo[nSearchIndex].path,
extraParameters: { extraParameters: {
sort: searchQueryParam ? '_score' : INITIAL_SORT_FIELD,
page: '1', page: '1',
quickFilter: commonQuickFilters quickFilter: commonQuickFilters
? JSON.stringify(commonQuickFilters) ? JSON.stringify(commonQuickFilters)
@ -161,7 +174,7 @@ const ExplorePageV1: FunctionComponent = () => {
}) })
); );
}, },
[commonQuickFilters] [commonQuickFilters, searchQueryParam]
); );
const handleQuickFilterChange = useCallback( const handleQuickFilterChange = useCallback(
@ -193,13 +206,13 @@ const ExplorePageV1: FunctionComponent = () => {
if (isNil(tabInfo)) { if (isNil(tabInfo)) {
const activeKey = findActiveSearchIndex(searchHitCounts); const activeKey = findActiveSearchIndex(searchHitCounts);
return activeKey ? activeKey : SearchIndex.TABLE; return activeKey ? activeKey : SearchIndex.DATA_PRODUCT;
} }
return tabInfo[0] as ExploreSearchIndex; return tabInfo[0] as ExploreSearchIndex;
} }
return SearchIndex.TABLE; return SearchIndex.DATA_PRODUCT;
}, [tab, searchHitCounts]); }, [tab, searchHitCounts]);
const tabItems = useMemo(() => { const tabItems = useMemo(() => {
@ -297,19 +310,13 @@ const ExplorePageV1: FunctionComponent = () => {
queryFilter as unknown as QueryFilterInterface queryFilter as unknown as QueryFilterInterface
); );
let newSortValue = sortValue;
if (searchQueryParam !== '') {
newSortValue = '_score';
setSortValue(newSortValue);
}
setIsLoading(true); setIsLoading(true);
Promise.all([ Promise.all([
searchQuery({ searchQuery({
query: escapeESReservedCharacters(searchQueryParam), query: escapeESReservedCharacters(searchQueryParam),
searchIndex, searchIndex,
queryFilter: combinedQueryFilter, queryFilter: combinedQueryFilter,
sortField: newSortValue, sortField: sortValue,
sortOrder, sortOrder,
pageNumber: page, pageNumber: page,
pageSize: size, pageSize: size,
@ -322,6 +329,7 @@ const ExplorePageV1: FunctionComponent = () => {
}), }),
Promise.all( Promise.all(
[ [
SearchIndex.DATA_PRODUCT,
SearchIndex.TABLE, SearchIndex.TABLE,
SearchIndex.TOPIC, SearchIndex.TOPIC,
SearchIndex.DASHBOARD, SearchIndex.DASHBOARD,
@ -348,6 +356,7 @@ const ExplorePageV1: FunctionComponent = () => {
) )
).then( ).then(
([ ([
dataProductResponse,
tableResponse, tableResponse,
topicResponse, topicResponse,
dashboardResponse, dashboardResponse,
@ -361,6 +370,7 @@ const ExplorePageV1: FunctionComponent = () => {
searchIndexResponse, searchIndexResponse,
]) => { ]) => {
setSearchHitCounts({ setSearchHitCounts({
[SearchIndex.DATA_PRODUCT]: dataProductResponse.hits.total.value,
[SearchIndex.TABLE]: tableResponse.hits.total.value, [SearchIndex.TABLE]: tableResponse.hits.total.value,
[SearchIndex.TOPIC]: topicResponse.hits.total.value, [SearchIndex.TOPIC]: topicResponse.hits.total.value,
[SearchIndex.DASHBOARD]: dashboardResponse.hits.total.value, [SearchIndex.DASHBOARD]: dashboardResponse.hits.total.value,
@ -440,9 +450,8 @@ const ExplorePageV1: FunctionComponent = () => {
handlePageChange(1); handlePageChange(1);
setSortOrder(sort); setSortOrder(sort);
}} }}
onChangeSortValue={(sort) => { onChangeSortValue={(sortVal) => {
handlePageChange(1); handleSortValueChange(1, sortVal);
setSortValue(sort);
}} }}
/> />
); );

View File

@ -23,6 +23,7 @@ import { User } from '../generated/entity/teams/user';
import { SearchResponse } from '../interface/search.interface'; import { SearchResponse } from '../interface/search.interface';
export type SearchEntityHits = SearchResponse< export type SearchEntityHits = SearchResponse<
| SearchIndex.DATA_PRODUCT
| SearchIndex.PIPELINE | SearchIndex.PIPELINE
| SearchIndex.DASHBOARD | SearchIndex.DASHBOARD
| SearchIndex.TABLE | SearchIndex.TABLE
@ -56,7 +57,7 @@ export const formatDataResponse = (
newData.owner = get(hit, '_source.owner'); newData.owner = get(hit, '_source.owner');
newData.highlight = hit.highlight; newData.highlight = hit.highlight;
newData.entityType = hit._source.entityType; newData.entityType = hit._source.entityType;
newData.deleted = hit._source.deleted; newData.deleted = get(hit, '_source.deleted');
if ('tableType' in source) { if ('tableType' in source) {
newData.tableType = source.tableType ?? ''; newData.tableType = source.tableType ?? '';

View File

@ -24,6 +24,7 @@ import {
CONTAINER_DROPDOWN_ITEMS, CONTAINER_DROPDOWN_ITEMS,
DASHBOARD_DATA_MODEL_TYPE, DASHBOARD_DATA_MODEL_TYPE,
DASHBOARD_DROPDOWN_ITEMS, DASHBOARD_DROPDOWN_ITEMS,
DATA_PRODUCT_DROPDOWN_ITEMS,
GLOSSARY_DROPDOWN_ITEMS, GLOSSARY_DROPDOWN_ITEMS,
PIPELINE_DROPDOWN_ITEMS, PIPELINE_DROPDOWN_ITEMS,
SEARCH_INDEX_DROPDOWN_ITEMS, SEARCH_INDEX_DROPDOWN_ITEMS,
@ -79,6 +80,8 @@ export const getDropDownItems = (index: string) => {
return [...GLOSSARY_DROPDOWN_ITEMS]; return [...GLOSSARY_DROPDOWN_ITEMS];
case SearchIndex.TAG: case SearchIndex.TAG:
return [...TAG_DROPDOWN_ITEMS]; return [...TAG_DROPDOWN_ITEMS];
case SearchIndex.DATA_PRODUCT:
return [...DATA_PRODUCT_DROPDOWN_ITEMS];
default: default:
return []; return [];

View File

@ -109,3 +109,23 @@ export const getUserNames = (
return getOwner(hasPermission, getEntityName(entity.owner), entity.owner); return getOwner(hasPermission, getEntityName(entity.owner), entity.owner);
}; };
export const getQueryFilterToIncludeDomain = (fqn: string) => ({
query: {
bool: {
must: [
{
bool: {
must: [
{
term: {
'domain.fullyQualifiedName': fqn,
},
},
],
},
},
],
},
},
});