mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-09-25 17:04:54 +00:00
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:
parent
c65518ac04
commit
10ef83779e
@ -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 |
@ -240,6 +240,9 @@ const Appbar: React.FC = (): JSX.Element => {
|
||||
tab: defaultTab,
|
||||
search: value,
|
||||
isPersistFilters: false,
|
||||
extraParameters: {
|
||||
sort: '_score',
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
@ -38,6 +38,7 @@ export interface AssetSelectionModalProps {
|
||||
type?: AssetsOfEntity;
|
||||
onCancel: () => void;
|
||||
onSave?: () => void;
|
||||
queryFilter?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export type AssetsUnion =
|
||||
|
@ -38,7 +38,6 @@ import {
|
||||
getAssetsSearchIndex,
|
||||
getEntityAPIfromSource,
|
||||
} from '../../../utils/Assets/AssetsUtils';
|
||||
import { getQueryFilterToExcludeTerm } from '../../../utils/GlossaryUtils';
|
||||
import Searchbar from '../../common/searchbar/Searchbar';
|
||||
import TableDataCardV2 from '../../common/table-data-card-v2/TableDataCardV2';
|
||||
import { AssetsOfEntity } from '../../Glossary/GlossaryTerms/tabs/AssetsTabs.interface';
|
||||
@ -53,6 +52,7 @@ export const AssetSelectionModal = ({
|
||||
onSave,
|
||||
open,
|
||||
type = AssetsOfEntity.GLOSSARY,
|
||||
queryFilter = {},
|
||||
}: AssetSelectionModalProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [search, setSearch] = useState('');
|
||||
@ -67,14 +67,6 @@ export const AssetSelectionModal = ({
|
||||
const [pageNumber, setPageNumber] = useState(1);
|
||||
const [totalCount, setTotalCount] = useState(0);
|
||||
|
||||
const queryFilter = useMemo(() => {
|
||||
if (type === AssetsOfEntity.GLOSSARY) {
|
||||
return getQueryFilterToExcludeTerm(entityFqn);
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
}, [entityFqn, type]);
|
||||
|
||||
const fetchEntities = useCallback(
|
||||
async ({ searchText = '', page = 1, index = activeFilter }) => {
|
||||
try {
|
||||
@ -106,7 +98,7 @@ export const AssetSelectionModal = ({
|
||||
} else if (type === AssetsOfEntity.DATA_PRODUCT) {
|
||||
const data = await getDataProductByName(
|
||||
encodeURIComponent(entityFqn),
|
||||
''
|
||||
'domain'
|
||||
);
|
||||
setActiveEntity(data);
|
||||
}
|
||||
|
@ -44,10 +44,7 @@ import { EntityDetailsObjectInterface } from '../../../components/Explore/explor
|
||||
import AssetsTabs, {
|
||||
AssetsTabRef,
|
||||
} from '../../../components/Glossary/GlossaryTerms/tabs/AssetsTabs.component';
|
||||
import {
|
||||
AssetsOfEntity,
|
||||
AssetsViewType,
|
||||
} from '../../../components/Glossary/GlossaryTerms/tabs/AssetsTabs.interface';
|
||||
import { AssetsOfEntity } from '../../../components/Glossary/GlossaryTerms/tabs/AssetsTabs.interface';
|
||||
import EntityDeleteModal from '../../../components/Modals/EntityDeleteModal/EntityDeleteModal';
|
||||
import EntityNameModal from '../../../components/Modals/EntityNameModal/EntityNameModal.component';
|
||||
import { usePermissionProvider } from '../../../components/PermissionProvider/PermissionProvider';
|
||||
@ -56,7 +53,6 @@ import {
|
||||
ResourceEntity,
|
||||
} from '../../../components/PermissionProvider/PermissionProvider.interface';
|
||||
import TabsLabel from '../../../components/TabsLabel/TabsLabel.component';
|
||||
import { FQN_SEPARATOR_CHAR } from '../../../constants/char.constants';
|
||||
import { DE_ACTIVE_COLOR } from '../../../constants/constants';
|
||||
import { EntityField } from '../../../constants/Feeds.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 { searchData } from '../../../rest/miscAPI';
|
||||
import { getEntityDeleteMessage } from '../../../utils/CommonUtils';
|
||||
import { getQueryFilterToIncludeDomain } from '../../../utils/DomainUtils';
|
||||
import { getEntityName } from '../../../utils/EntityUtils';
|
||||
import { getEntityVersionByField } from '../../../utils/EntityVersionUtils';
|
||||
import Fqn from '../../../utils/Fqn.js';
|
||||
import {
|
||||
checkPermission,
|
||||
DEFAULT_ENTITY_PERMISSION,
|
||||
@ -117,25 +114,18 @@ const DataProductsDetailsPage = ({
|
||||
const [assetCount, setAssetCount] = useState<number>(0);
|
||||
|
||||
const breadcrumbs = useMemo(() => {
|
||||
if (!dataProductFqn) {
|
||||
if (!dataProduct.domain) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const arr = Fqn.split(dataProductFqn);
|
||||
const dataFQN: Array<string> = [];
|
||||
|
||||
return [
|
||||
...arr.slice(0, -1).map((d) => {
|
||||
dataFQN.push(d);
|
||||
|
||||
return {
|
||||
name: d,
|
||||
url: getDomainPath(dataFQN.join(FQN_SEPARATOR_CHAR)),
|
||||
activeTitle: false,
|
||||
};
|
||||
}),
|
||||
{
|
||||
name: getEntityName(dataProduct.domain),
|
||||
url: getDomainPath(dataProduct.domain.fullyQualifiedName),
|
||||
activeTitle: false,
|
||||
},
|
||||
];
|
||||
}, [dataProductFqn]);
|
||||
}, [dataProduct.domain]);
|
||||
|
||||
const [name, displayName] = useMemo(() => {
|
||||
const defaultName = dataProduct.name;
|
||||
@ -425,7 +415,6 @@ const DataProductsDetailsPage = ({
|
||||
permissions={dataProductPermission}
|
||||
ref={assetTabRef}
|
||||
type={AssetsOfEntity.DATA_PRODUCT}
|
||||
viewType={AssetsViewType.TABS}
|
||||
onAddAsset={() => setAssetModelVisible(true)}
|
||||
onAssetClick={handleAssetClick}
|
||||
/>
|
||||
@ -575,6 +564,9 @@ const DataProductsDetailsPage = ({
|
||||
<AssetSelectionModal
|
||||
entityFqn={dataProductFqn}
|
||||
open={assetModalVisible}
|
||||
queryFilter={getQueryFilterToIncludeDomain(
|
||||
dataProduct.domain?.fullyQualifiedName ?? ''
|
||||
)}
|
||||
type={AssetsOfEntity.DATA_PRODUCT}
|
||||
onCancel={() => setAssetModelVisible(false)}
|
||||
onSave={handleAssetSave}
|
||||
|
@ -40,6 +40,5 @@
|
||||
}
|
||||
.summary-panel-container {
|
||||
height: @domain-page-height;
|
||||
border-left: 1px solid @border-color;
|
||||
}
|
||||
}
|
||||
|
@ -101,7 +101,7 @@ const DataProductsPage = () => {
|
||||
try {
|
||||
const data = await getDataProductByName(
|
||||
encodeURIComponent(fqn),
|
||||
'owner,experts'
|
||||
'domain,owner,experts'
|
||||
);
|
||||
setDataProduct(data);
|
||||
|
||||
|
@ -11,7 +11,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { isNil, isString } from 'lodash';
|
||||
import { cloneDeep, isNil, isString } from 'lodash';
|
||||
import Qs from 'qs';
|
||||
import React, {
|
||||
useCallback,
|
||||
@ -22,9 +22,11 @@ import React, {
|
||||
} from 'react';
|
||||
import {
|
||||
Config,
|
||||
FieldGroup,
|
||||
ImmutableTree,
|
||||
JsonTree,
|
||||
Utils as QbUtils,
|
||||
ValueSource,
|
||||
} from 'react-awesome-query-builder';
|
||||
import { useHistory, useLocation, useParams } from 'react-router-dom';
|
||||
import {
|
||||
@ -33,7 +35,9 @@ import {
|
||||
} from '../../../constants/AdvancedSearch.constants';
|
||||
import { tabsInfo } from '../../../constants/explore.constants';
|
||||
import { SearchIndex } from '../../../enums/search.enum';
|
||||
import { getTypeByFQN } from '../../../rest/metadataTypeAPI';
|
||||
import { elasticSearchFormat } from '../../../utils/QueryBuilderElasticsearchFormatUtils';
|
||||
import { getEntityTypeFromSearchIndex } from '../../../utils/SearchUtils';
|
||||
import Loader from '../../Loader/Loader';
|
||||
import { AdvancedSearchModal } from '../AdvanceSearchModal.component';
|
||||
import { ExploreSearchIndex, UrlParams } from '../explore.interface';
|
||||
@ -165,15 +169,55 @@ export const AdvanceSearchProvider = ({
|
||||
});
|
||||
}, [history, location.pathname]);
|
||||
|
||||
useEffect(() => {
|
||||
if (jsonTree) {
|
||||
const tree = QbUtils.checkTree(QbUtils.loadTree(jsonTree), config);
|
||||
async function getCustomAttributesSubfields() {
|
||||
const updatedConfig = cloneDeep(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);
|
||||
const qFilter = {
|
||||
query: elasticSearchFormat(tree, config),
|
||||
query: elasticSearchFormat(tree, updatedConfig),
|
||||
};
|
||||
setQueryFilter(qFilter);
|
||||
setSQLQuery(QbUtils.sqlFormat(tree, config) ?? '');
|
||||
setSQLQuery(QbUtils.sqlFormat(tree, updatedConfig) ?? '');
|
||||
},
|
||||
[config]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (jsonTree) {
|
||||
loadTree(jsonTree);
|
||||
} else {
|
||||
handleReset();
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { DataProduct } from '../../../../generated/entity/domains/dataProduct';
|
||||
import { getEntityName } from '../../../../utils/EntityUtils';
|
||||
import { OwnerLabel } from '../../../common/OwnerLabel/OwnerLabel.component';
|
||||
import RichTextEditorPreviewer from '../../../common/rich-text-editor/RichTextEditorPreviewer';
|
||||
import SummaryPanelSkeleton from '../../../Skeleton/SummaryPanelSkeleton/SummaryPanelSkeleton.component';
|
||||
|
||||
interface DataProductSummaryProps {
|
||||
@ -50,6 +51,32 @@ const DataProductSummary = ({
|
||||
</Row>
|
||||
<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]}>
|
||||
<Col span={24}>
|
||||
<Typography.Text
|
||||
|
@ -268,6 +268,11 @@ export const MOCK_EXPLORE_SEARCH_RESULTS: SearchResponse<ExploreSearchIndex> = {
|
||||
};
|
||||
|
||||
export const MOCK_EXPLORE_TAB_ITEMS = [
|
||||
{
|
||||
key: 'data_product_search_index',
|
||||
label: 'data_product_search_index',
|
||||
count: 0,
|
||||
},
|
||||
{
|
||||
key: 'table_search_index',
|
||||
label: 'table_search_index',
|
||||
|
@ -46,6 +46,7 @@ export type UrlParams = {
|
||||
};
|
||||
|
||||
export type ExploreSearchIndex =
|
||||
| SearchIndex.DATA_PRODUCT
|
||||
| SearchIndex.TABLE
|
||||
| SearchIndex.PIPELINE
|
||||
| SearchIndex.DASHBOARD
|
||||
|
@ -101,7 +101,8 @@ const ExploreSearchCard: React.FC<ExploreSearchCardProps> = forwardRef<
|
||||
|
||||
if (
|
||||
source.entityType !== EntityType.GLOSSARY_TERM &&
|
||||
source.entityType !== EntityType.TAG
|
||||
source.entityType !== EntityType.TAG &&
|
||||
source.entityType !== EntityType.DATA_PRODUCT
|
||||
) {
|
||||
_otherDetails.push({
|
||||
key: 'Tier',
|
||||
|
@ -76,7 +76,6 @@ const ExploreV1: React.FC<ExploreProps> = ({
|
||||
quickFilters,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
// const { tab } = useParams<{ tab: string }>();
|
||||
const [selectedQuickFilters, setSelectedQuickFilters] = useState<
|
||||
ExploreQuickFilterField[]
|
||||
>([] as ExploreQuickFilterField[]);
|
||||
|
@ -67,6 +67,7 @@ const props = {
|
||||
tabItems: MOCK_EXPLORE_TAB_ITEMS,
|
||||
activeTabKey: SearchIndex.TABLE,
|
||||
tabCounts: {
|
||||
data_product_search_index: 0,
|
||||
table_search_index: 20,
|
||||
topic_search_index: 10,
|
||||
dashboard_search_index: 14,
|
||||
|
@ -101,10 +101,15 @@ export const FeedEditor = forwardRef<editorRef, FeedEditorProp>(
|
||||
renderLoading: () => `${t('label.loading')}...`,
|
||||
renderItem: (item: Record<string, any>) => {
|
||||
if (!item.type) {
|
||||
return `<div class="d-flex gap-2">
|
||||
const userResult = `<div class="d-flex gap-2">
|
||||
${item.avatarEle}
|
||||
<span class="d-flex items-center truncate w-56">${item.name}</span>
|
||||
</div>`;
|
||||
|
||||
const userWrapper = document.createElement('div');
|
||||
userWrapper.innerHTML = userResult;
|
||||
|
||||
return userWrapper;
|
||||
}
|
||||
|
||||
const breadcrumbsData = item.breadcrumbs
|
||||
@ -127,7 +132,7 @@ export const FeedEditor = forwardRef<editorRef, FeedEditorProp>(
|
||||
? `<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>
|
||||
${breadcrumbEle}
|
||||
@ -137,6 +142,11 @@ export const FeedEditor = forwardRef<editorRef, FeedEditorProp>(
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.innerHTML = result;
|
||||
|
||||
return wrapper;
|
||||
},
|
||||
},
|
||||
markdownOptions: {},
|
||||
|
@ -25,6 +25,7 @@ import { ChangeDescription } from '../../../generated/entity/type';
|
||||
import { searchData } from '../../../rest/miscAPI';
|
||||
import { getCountBadge, getFeedCounts } from '../../../utils/CommonUtils';
|
||||
import { getEntityVersionByField } from '../../../utils/EntityVersionUtils';
|
||||
import { getQueryFilterToExcludeTerm } from '../../../utils/GlossaryUtils';
|
||||
import { getGlossaryTermsVersionsPath } from '../../../utils/RouterUtils';
|
||||
import { getEncodedFqn } from '../../../utils/StringsUtils';
|
||||
import { ActivityFeedTab } from '../../ActivityFeed/ActivityFeedTab/ActivityFeedTab.component';
|
||||
@ -321,6 +322,9 @@ const GlossaryTermsV1 = ({
|
||||
<AssetSelectionModal
|
||||
entityFqn={glossaryTerm.fullyQualifiedName}
|
||||
open={assetModalVisible}
|
||||
queryFilter={getQueryFilterToExcludeTerm(
|
||||
glossaryTerm.fullyQualifiedName
|
||||
)}
|
||||
type={AssetsOfEntity.GLOSSARY}
|
||||
onCancel={() => setAssetModelVisible(false)}
|
||||
onSave={handleAssetSave}
|
||||
|
@ -12,8 +12,6 @@
|
||||
*/
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import AppState from '../../AppState';
|
||||
import { User } from '../../generated/entity/teams/user';
|
||||
import {
|
||||
getEntityPermissionByFqn,
|
||||
getEntityPermissionById,
|
||||
@ -37,13 +35,25 @@ jest.mock('../../rest/permissionAPI', () => ({
|
||||
.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', () => {
|
||||
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(
|
||||
<PermissionProvider>
|
||||
<div data-testid="children">Children</div>
|
||||
@ -57,15 +67,10 @@ describe('PermissionProvider', () => {
|
||||
expect(getResourcePermission).not.toHaveBeenCalled();
|
||||
|
||||
expect(await screen.findByTestId('children')).toBeInTheDocument();
|
||||
|
||||
getUserDetailsSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('Should not call apis when current user is undefined', async () => {
|
||||
const getUserDetailsSpy = jest
|
||||
.spyOn(AppState, 'getCurrentUserDetails')
|
||||
.mockReturnValue(undefined);
|
||||
|
||||
currentUser = null;
|
||||
render(
|
||||
<PermissionProvider>
|
||||
<div data-testid="children">Children</div>
|
||||
@ -77,7 +82,5 @@ describe('PermissionProvider', () => {
|
||||
expect(getEntityPermissionById).not.toHaveBeenCalled();
|
||||
expect(getEntityPermissionByFqn).not.toHaveBeenCalled();
|
||||
expect(getResourcePermission).not.toHaveBeenCalled();
|
||||
|
||||
getUserDetailsSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
@ -24,7 +24,6 @@ import React, {
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import AppState from '../../AppState';
|
||||
import Loader from '../../components/Loader/Loader';
|
||||
import { REDIRECT_PATHNAME } from '../../constants/constants';
|
||||
import {
|
||||
@ -41,6 +40,7 @@ import {
|
||||
getOperationPermissions,
|
||||
getUIPermission,
|
||||
} from '../../utils/PermissionsUtils';
|
||||
import { useAuthContext } from '../authentication/auth-provider/AuthProvider';
|
||||
import {
|
||||
EntityPermissionMap,
|
||||
PermissionContextType,
|
||||
@ -67,6 +67,7 @@ const PermissionProvider: FC<PermissionProviderProps> = ({ children }) => {
|
||||
const [permissions, setPermissions] = useState<UIPermission>(
|
||||
{} as UIPermission
|
||||
);
|
||||
const { currentUser } = useAuthContext();
|
||||
const cookieStorage = new CookieStorage();
|
||||
const history = useHistory();
|
||||
const [loading, setLoading] = useState(false);
|
||||
@ -78,11 +79,6 @@ const PermissionProvider: FC<PermissionProviderProps> = ({ children }) => {
|
||||
{} as UIPermission
|
||||
);
|
||||
|
||||
// Update current user details of AppState change
|
||||
const currentUser = useMemo(() => {
|
||||
return AppState.getCurrentUserDetails();
|
||||
}, [AppState.userDetails, AppState.nonSecureUserDetails]);
|
||||
|
||||
const redirectToStoredPath = useCallback(() => {
|
||||
const urlPathname = cookieStorage.getItem(REDIRECT_PATHNAME);
|
||||
if (urlPathname) {
|
||||
|
@ -29,7 +29,7 @@
|
||||
}
|
||||
.ant-tabs-nav {
|
||||
padding: 8px 16px 0 28px;
|
||||
margin-bottom: 0;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.widget-manage-dropdown .ant-dropdown-trigger {
|
||||
|
@ -84,25 +84,21 @@ const EntitySummaryDetails = ({
|
||||
/>
|
||||
);
|
||||
|
||||
const {
|
||||
isEntityDetails,
|
||||
userDetails,
|
||||
isTier,
|
||||
isOwner,
|
||||
const { isEntityDetails, userDetails, isTier, isOwner, isTeamOwner } =
|
||||
useMemo(() => {
|
||||
const userDetails = getTeamsUser(data);
|
||||
|
||||
isTeamOwner,
|
||||
} = useMemo(() => {
|
||||
const userDetails = getTeamsUser(data);
|
||||
|
||||
return {
|
||||
isEntityCard: data?.isEntityCard,
|
||||
isEntityDetails: data?.isEntityDetails,
|
||||
userDetails,
|
||||
isTier: data.key === 'Tier',
|
||||
isOwner: data.key === 'Owner',
|
||||
isTeamOwner: isString(data.value) ? data.value.includes('teams/') : false,
|
||||
};
|
||||
}, [data]);
|
||||
return {
|
||||
isEntityCard: data?.isEntityCard,
|
||||
isEntityDetails: data?.isEntityDetails,
|
||||
userDetails,
|
||||
isTier: data.key === 'Tier',
|
||||
isOwner: data.key === 'Owner',
|
||||
isTeamOwner: isString(data.value)
|
||||
? data.value.includes('teams/')
|
||||
: false,
|
||||
};
|
||||
}, [data]);
|
||||
|
||||
switch (data.key) {
|
||||
case 'Owner':
|
||||
@ -129,7 +125,11 @@ const EntitySummaryDetails = ({
|
||||
</>
|
||||
)}
|
||||
{isTeamOwner ? (
|
||||
<IconTeamsGrey height={18} width={18} />
|
||||
<IconTeamsGrey
|
||||
className="align-middle"
|
||||
height={18}
|
||||
width={18}
|
||||
/>
|
||||
) : (
|
||||
<ProfilePicture
|
||||
displayName={displayVal}
|
||||
|
@ -10,11 +10,11 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { FilterOutlined } from '@ant-design/icons';
|
||||
import { Button, Checkbox, List, Popover, Space, Typography } from 'antd';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import AppState from '../../../AppState';
|
||||
import { ReactComponent as FilterIcon } from '../../../assets/svg/ic-feeds-filter.svg';
|
||||
import { FeedFilter } from '../../../enums/mydata.enum';
|
||||
import './feeds-filter-popover.less';
|
||||
import { FeedsFilterPopoverProps } from './FeedsFilterPopover.interface';
|
||||
@ -126,7 +126,7 @@ const FeedsFilterPopover = ({
|
||||
showArrow={false}
|
||||
trigger="click"
|
||||
onOpenChange={setPopupVisible}>
|
||||
<Button icon={<FilterOutlined />}>{t('label.filter-plural')}</Button>
|
||||
<Button className="flex-center" icon={<FilterIcon height={16} />} />
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
@ -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 = [
|
||||
...COMMON_DROPDOWN_ITEMS,
|
||||
...TABLE_DROPDOWN_ITEMS,
|
||||
|
@ -26,7 +26,7 @@ export const SIDEBAR_LIST = [
|
||||
{
|
||||
key: ROUTES.EXPLORE,
|
||||
label: i18next.t('label.explore'),
|
||||
redirect_url: '/explore/tables',
|
||||
redirect_url: '/explore/dataProducts',
|
||||
icon: ExploreIcon,
|
||||
dataTestId: 'app-bar-item-explore',
|
||||
},
|
||||
|
@ -56,6 +56,12 @@ export interface 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]: {
|
||||
label: i18n.t('label.table-plural'),
|
||||
sortingFields: tableSortingFields,
|
||||
|
@ -1917,6 +1917,7 @@ export const mockSearchData = {
|
||||
};
|
||||
|
||||
export const MOCK_EXPLORE_PAGE_COUNT = {
|
||||
[SearchIndex.DATA_PRODUCT]: 0,
|
||||
[SearchIndex.TABLE]: mockSearchData.hits.total.value,
|
||||
[SearchIndex.TOPIC]: 0,
|
||||
[SearchIndex.DASHBOARD]: 0,
|
||||
|
@ -77,8 +77,6 @@ const ExplorePageV1: FunctionComponent = () => {
|
||||
const [advancesSearchQuickFilters, setAdvancedSearchQuickFilters] =
|
||||
useState<QueryFilterInterface>();
|
||||
|
||||
const [sortValue, setSortValue] = useState<string>(INITIAL_SORT_FIELD);
|
||||
|
||||
const [sortOrder, setSortOrder] = useState<SORT_ORDER>(SORT_ORDER.DESC);
|
||||
|
||||
const [searchHitCounts, setSearchHitCounts] = useState<SearchHitCounts>();
|
||||
@ -87,20 +85,23 @@ const ExplorePageV1: FunctionComponent = () => {
|
||||
|
||||
const { queryFilter } = useAdvanceSearch();
|
||||
|
||||
const parsedSearch = useMemo(
|
||||
() =>
|
||||
Qs.parse(
|
||||
location.search.startsWith('?')
|
||||
? location.search.substring(1)
|
||||
: location.search
|
||||
),
|
||||
[location.search]
|
||||
);
|
||||
const [parsedSearch, searchQueryParam, sortValue] = useMemo(() => {
|
||||
const parsedSearch = Qs.parse(
|
||||
location.search.startsWith('?')
|
||||
? location.search.substring(1)
|
||||
: location.search
|
||||
);
|
||||
|
||||
const searchQueryParam = useMemo(
|
||||
() => (isString(parsedSearch.search) ? parsedSearch.search : ''),
|
||||
[location.search]
|
||||
);
|
||||
const searchQueryParam = isString(parsedSearch.search)
|
||||
? parsedSearch.search
|
||||
: '';
|
||||
|
||||
const sortValue = isString(parsedSearch.sort)
|
||||
? parsedSearch.sort
|
||||
: INITIAL_SORT_FIELD;
|
||||
|
||||
return [parsedSearch, searchQueryParam, sortValue];
|
||||
}, [location.search]);
|
||||
|
||||
const handlePageChange: ExploreProps['onChangePage'] = (page, size) => {
|
||||
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.
|
||||
const commonQuickFilters = useMemo(() => {
|
||||
const mustField: QueryFieldInterface[] = get(
|
||||
@ -152,6 +164,7 @@ const ExplorePageV1: FunctionComponent = () => {
|
||||
getExplorePath({
|
||||
tab: tabsInfo[nSearchIndex].path,
|
||||
extraParameters: {
|
||||
sort: searchQueryParam ? '_score' : INITIAL_SORT_FIELD,
|
||||
page: '1',
|
||||
quickFilter: commonQuickFilters
|
||||
? JSON.stringify(commonQuickFilters)
|
||||
@ -161,7 +174,7 @@ const ExplorePageV1: FunctionComponent = () => {
|
||||
})
|
||||
);
|
||||
},
|
||||
[commonQuickFilters]
|
||||
[commonQuickFilters, searchQueryParam]
|
||||
);
|
||||
|
||||
const handleQuickFilterChange = useCallback(
|
||||
@ -193,13 +206,13 @@ const ExplorePageV1: FunctionComponent = () => {
|
||||
if (isNil(tabInfo)) {
|
||||
const activeKey = findActiveSearchIndex(searchHitCounts);
|
||||
|
||||
return activeKey ? activeKey : SearchIndex.TABLE;
|
||||
return activeKey ? activeKey : SearchIndex.DATA_PRODUCT;
|
||||
}
|
||||
|
||||
return tabInfo[0] as ExploreSearchIndex;
|
||||
}
|
||||
|
||||
return SearchIndex.TABLE;
|
||||
return SearchIndex.DATA_PRODUCT;
|
||||
}, [tab, searchHitCounts]);
|
||||
|
||||
const tabItems = useMemo(() => {
|
||||
@ -297,19 +310,13 @@ const ExplorePageV1: FunctionComponent = () => {
|
||||
queryFilter as unknown as QueryFilterInterface
|
||||
);
|
||||
|
||||
let newSortValue = sortValue;
|
||||
if (searchQueryParam !== '') {
|
||||
newSortValue = '_score';
|
||||
setSortValue(newSortValue);
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
Promise.all([
|
||||
searchQuery({
|
||||
query: escapeESReservedCharacters(searchQueryParam),
|
||||
searchIndex,
|
||||
queryFilter: combinedQueryFilter,
|
||||
sortField: newSortValue,
|
||||
sortField: sortValue,
|
||||
sortOrder,
|
||||
pageNumber: page,
|
||||
pageSize: size,
|
||||
@ -322,6 +329,7 @@ const ExplorePageV1: FunctionComponent = () => {
|
||||
}),
|
||||
Promise.all(
|
||||
[
|
||||
SearchIndex.DATA_PRODUCT,
|
||||
SearchIndex.TABLE,
|
||||
SearchIndex.TOPIC,
|
||||
SearchIndex.DASHBOARD,
|
||||
@ -348,6 +356,7 @@ const ExplorePageV1: FunctionComponent = () => {
|
||||
)
|
||||
).then(
|
||||
([
|
||||
dataProductResponse,
|
||||
tableResponse,
|
||||
topicResponse,
|
||||
dashboardResponse,
|
||||
@ -361,6 +370,7 @@ const ExplorePageV1: FunctionComponent = () => {
|
||||
searchIndexResponse,
|
||||
]) => {
|
||||
setSearchHitCounts({
|
||||
[SearchIndex.DATA_PRODUCT]: dataProductResponse.hits.total.value,
|
||||
[SearchIndex.TABLE]: tableResponse.hits.total.value,
|
||||
[SearchIndex.TOPIC]: topicResponse.hits.total.value,
|
||||
[SearchIndex.DASHBOARD]: dashboardResponse.hits.total.value,
|
||||
@ -440,9 +450,8 @@ const ExplorePageV1: FunctionComponent = () => {
|
||||
handlePageChange(1);
|
||||
setSortOrder(sort);
|
||||
}}
|
||||
onChangeSortValue={(sort) => {
|
||||
handlePageChange(1);
|
||||
setSortValue(sort);
|
||||
onChangeSortValue={(sortVal) => {
|
||||
handleSortValueChange(1, sortVal);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
@ -23,6 +23,7 @@ import { User } from '../generated/entity/teams/user';
|
||||
import { SearchResponse } from '../interface/search.interface';
|
||||
|
||||
export type SearchEntityHits = SearchResponse<
|
||||
| SearchIndex.DATA_PRODUCT
|
||||
| SearchIndex.PIPELINE
|
||||
| SearchIndex.DASHBOARD
|
||||
| SearchIndex.TABLE
|
||||
@ -56,7 +57,7 @@ export const formatDataResponse = (
|
||||
newData.owner = get(hit, '_source.owner');
|
||||
newData.highlight = hit.highlight;
|
||||
newData.entityType = hit._source.entityType;
|
||||
newData.deleted = hit._source.deleted;
|
||||
newData.deleted = get(hit, '_source.deleted');
|
||||
|
||||
if ('tableType' in source) {
|
||||
newData.tableType = source.tableType ?? '';
|
||||
|
@ -24,6 +24,7 @@ import {
|
||||
CONTAINER_DROPDOWN_ITEMS,
|
||||
DASHBOARD_DATA_MODEL_TYPE,
|
||||
DASHBOARD_DROPDOWN_ITEMS,
|
||||
DATA_PRODUCT_DROPDOWN_ITEMS,
|
||||
GLOSSARY_DROPDOWN_ITEMS,
|
||||
PIPELINE_DROPDOWN_ITEMS,
|
||||
SEARCH_INDEX_DROPDOWN_ITEMS,
|
||||
@ -79,6 +80,8 @@ export const getDropDownItems = (index: string) => {
|
||||
return [...GLOSSARY_DROPDOWN_ITEMS];
|
||||
case SearchIndex.TAG:
|
||||
return [...TAG_DROPDOWN_ITEMS];
|
||||
case SearchIndex.DATA_PRODUCT:
|
||||
return [...DATA_PRODUCT_DROPDOWN_ITEMS];
|
||||
|
||||
default:
|
||||
return [];
|
||||
|
@ -109,3 +109,23 @@ export const getUserNames = (
|
||||
|
||||
return getOwner(hasPermission, getEntityName(entity.owner), entity.owner);
|
||||
};
|
||||
|
||||
export const getQueryFilterToIncludeDomain = (fqn: string) => ({
|
||||
query: {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
term: {
|
||||
'domain.fullyQualifiedName': fqn,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user