diff --git a/datahub-web-react/src/app/entityV2/shared/components/styled/search/EmbeddedListSearch.tsx b/datahub-web-react/src/app/entityV2/shared/components/styled/search/EmbeddedListSearch.tsx deleted file mode 100644 index 12a02110de34e..0000000000000 --- a/datahub-web-react/src/app/entityV2/shared/components/styled/search/EmbeddedListSearch.tsx +++ /dev/null @@ -1,396 +0,0 @@ -import { ApolloError } from '@apollo/client'; -import React, { useEffect, useState } from 'react'; -import styled from 'styled-components'; -import { SearchCfg } from '../../../../../../conf'; -import { - useGetSearchCountLazyQuery, - useGetSearchResultsForMultipleQuery, -} from '../../../../../../graphql/search.generated'; -import { useGetViewQuery } from '../../../../../../graphql/view.generated'; -import { - EntityType, - FacetFilterInput, - FacetMetadata, - SearchAcrossEntitiesInput, - SortCriterion, -} from '../../../../../../types.generated'; -import analytics, { EventType } from '../../../../../analytics'; -import { useUserContext } from '../../../../../context/useUserContext'; -import { useEntityContext } from '../../../../../entity/shared/EntityContext'; -import { EntityAndType } from '../../../../../entity/shared/types'; -import { DEGREE_FILTER_NAME, UnionType } from '../../../../../search/utils/constants'; -import { mergeFilterSets } from '../../../../../search/utils/filterUtils'; -import { generateOrFilters } from '../../../../../search/utils/generateOrFilters'; -import { - DownloadSearchResults, - DownloadSearchResultsInput, - DownloadSearchResultsParams, -} from '../../../../../search/utils/types'; -import { useDownloadScrollAcrossEntitiesSearchResults } from '../../../../../search/utils/useDownloadScrollAcrossEntitiesSearchResults'; -import { Message } from '../../../../../shared/Message'; -import { isListSubset } from '../../../utils'; -import EmbeddedListSearchHeader from './EmbeddedListSearchHeader'; -import { EmbeddedListSearchResults } from './EmbeddedListSearchResults'; -import { EntityActionProps } from './EntitySearchResults'; -import { FilterSet, GetSearchResultsParams, SearchResultsInterface } from './types'; - -const Container = styled.div` - display: flex; - flex-direction: column; - height: 100%; - overflow-y: hidden; -`; - -// this extracts the response from useGetSearchResultsForMultipleQuery into a common interface other search endpoints can also produce -function useWrappedSearchResults(params: GetSearchResultsParams) { - const { data, loading, error, refetch } = useGetSearchResultsForMultipleQuery(params); - return { - data: data?.searchAcrossEntities, - loading, - error, - refetch: (refetchParams: GetSearchResultsParams['variables']) => - refetch(refetchParams).then((res) => res.data.searchAcrossEntities), - }; -} - -// the addFixedQuery checks and generate the query as per params pass to embeddedListSearch -export const addFixedQuery = (baseQuery: string, fixedQuery: string, emptyQuery: string) => { - let finalQuery = ``; - if (baseQuery && fixedQuery) { - finalQuery = baseQuery.includes(fixedQuery) ? `${baseQuery}` : `(*${baseQuery}*) AND (${fixedQuery})`; - } else if (baseQuery) { - finalQuery = `${baseQuery}`; - } else if (fixedQuery) { - finalQuery = `${fixedQuery}`; - } else { - return emptyQuery || ''; - } - return finalQuery; -}; - -// Simply remove the fields that were marked as fixed from the facets that the server -// responds. -export const removeFixedFiltersFromFacets = (fixedFilters: FilterSet, facets: FacetMetadata[]) => { - const fixedFields = fixedFilters.filters.map((filter) => filter.field); - return facets.filter((facet) => !fixedFields.includes(facet.field)); -}; - -const useGetSearchCountQueryResult = (param) => { - const [getSearchResult, { data }] = useGetSearchCountLazyQuery(param); - - useEffect(() => { - getSearchResult(); - }, [getSearchResult]); - - return { - data: data?.searchAcrossEntities, - }; -}; - -type Props = { - query: string; - entityTypes?: EntityType[]; - page: number; - unionType: UnionType; - filters: FacetFilterInput[]; - onChangeQuery: (query) => void; - onChangeFilters: (filters) => void; - onChangePage: (page) => void; - onChangeUnionType: (unionType: UnionType) => void; - onTotalChanged?: (newTotal: number) => void; - emptySearchQuery?: string | null; - fixedFilters?: FilterSet; - fixedQuery?: string | null; - placeholderText?: string | null; - defaultShowFilters?: boolean; - defaultFilters?: Array; - searchBarStyle?: any; - searchBarInputStyle?: any; - entityAction?: React.FC; - skipCache?: boolean; - useGetSearchResults?: (params: GetSearchResultsParams) => { - data: SearchResultsInterface | undefined | null; - loading: boolean; - error: ApolloError | undefined; - refetch: (variables: GetSearchResultsParams['variables']) => Promise; - }; - useGetDownloadSearchResults?: (params: DownloadSearchResultsParams) => { - loading: boolean; - error: ApolloError | undefined; - searchResults: DownloadSearchResults | undefined | null; - refetch: (input: DownloadSearchResultsInput) => Promise; - }; - shouldRefetch?: boolean; - resetShouldRefetch?: () => void; - applyView?: boolean; - showFilterBar?: boolean; - sort?: SortCriterion; -}; - -export const EmbeddedListSearch = ({ - query, - entityTypes, - filters, - page, - unionType, - onChangeQuery, - onChangeFilters, - onChangePage, - onChangeUnionType, - onTotalChanged, - emptySearchQuery, - fixedFilters, - fixedQuery, - placeholderText, - defaultShowFilters, - defaultFilters, - searchBarStyle, - searchBarInputStyle, - entityAction, - skipCache, - useGetSearchResults = useWrappedSearchResults, - useGetDownloadSearchResults = useDownloadScrollAcrossEntitiesSearchResults, - shouldRefetch, - resetShouldRefetch, - applyView = false, - showFilterBar = true, - sort, -}: Props) => { - const userContext = useUserContext(); - - const { shouldRefetchEmbeddedListSearch, setShouldRefetchEmbeddedListSearch } = useEntityContext(); - // Adjust query based on props - const finalQuery: string = addFixedQuery(query as string, fixedQuery as string, emptySearchQuery as string); - - const baseFilters = { - unionType, - filters, - }; - - const finalFilters = - (fixedFilters && mergeFilterSets(fixedFilters, baseFilters)) || generateOrFilters(unionType, filters); - - const [showFilters, setShowFilters] = useState(defaultShowFilters || false); - const [isSelectMode, setIsSelectMode] = useState(false); - const [selectedEntities, setSelectedEntities] = useState([]); - const [numResultsPerPage, setNumResultsPerPage] = useState(SearchCfg.RESULTS_PER_PAGE); - const [defaultViewUrn, setDefaultViewUrn] = useState(); - const [selectedViewUrn, setSelectedViewUrn] = useState(); - - useEffect(() => { - setSelectedViewUrn(userContext.localState?.selectedViewUrn || undefined); - setDefaultViewUrn(userContext.localState?.selectedViewUrn || undefined); - }, [userContext.localState?.selectedViewUrn]); - - // This hook is simply used to generate a refetch callback that the DownloadAsCsv component can use to - // download the correct results given the current context. - // TODO: Use the loading indicator to log a message to the user should download to CSV fail. - // TODO: Revisit this pattern -- what can we push down? - - const { refetch: refetchForDownload } = useGetDownloadSearchResults({ - variables: { - input: { - types: entityTypes || [], - query, - count: SearchCfg.RESULTS_PER_PAGE, - orFilters: generateOrFilters(unionType, filters), - scrollId: null, - }, - }, - skip: true, - }); - - const searchInput: SearchAcrossEntitiesInput = { - types: entityTypes || [], - query: finalQuery, - start: (page - 1) * numResultsPerPage, - count: numResultsPerPage, - orFilters: finalFilters, - viewUrn: (applyView && selectedViewUrn) || undefined, - sortInput: sort ? { sortCriterion: sort } : undefined, - ...(skipCache && { searchFlags: { skipCache: true } }), - }; - - const { data, loading, error, refetch } = useGetSearchResults({ - variables: { input: searchInput }, - fetchPolicy: 'cache-first', - }); - - const useGetViewSearchData = (viewUrn: string | undefined) => { - return useGetSearchCountQueryResult({ - variables: { - input: { - ...searchInput, - viewUrn: viewUrn || undefined, - }, - }, - fetchPolicy: 'cache-first', - }); - }; - - const allSearchCount = useGetViewSearchData(undefined)?.data?.total; - const defaultViewCount = useGetViewSearchData(defaultViewUrn)?.data?.total; - const { data: viewData } = useGetViewQuery({ - variables: { - urn: defaultViewUrn || '', - }, - skip: !defaultViewUrn, - }); - - const view = (viewData?.entity?.__typename === 'DataHubView' && viewData?.entity) || undefined; - - useEffect(() => { - if (shouldRefetch && resetShouldRefetch) { - refetch({ - input: searchInput, - }); - resetShouldRefetch(); - } - }); - - useEffect(() => { - if (shouldRefetchEmbeddedListSearch) { - refetch({ - input: searchInput, - }); - setShouldRefetchEmbeddedListSearch?.(false); - } - }); - - useEffect(() => { - if (data?.total !== undefined && onTotalChanged) { - onTotalChanged(data?.total); - } - }, [data?.total, onTotalChanged]); - - const searchResultEntities = - data?.searchResults?.map((result) => ({ urn: result.entity.urn, type: result.entity.type })) || []; - const searchResultUrns = searchResultEntities.map((entity) => entity.urn); - const selectedEntityUrns = selectedEntities.map((entity) => entity.urn); - - useEffect(() => { - if (filters.length) { - setShowFilters(true); - } - }, [filters]); - - const onToggleFilters = () => { - setShowFilters(!showFilters); - }; - - /** - * Invoked when the "select all" checkbox is clicked. - * - * This method either adds the entire current page of search results to - * the list of selected entities, or removes the current page from the set of selected entities. - */ - const onChangeSelectAll = (selected: boolean) => { - if (selected) { - // Add current page of urns to the master selected entity list - const entitiesToAdd = searchResultEntities.filter( - (entity) => - selectedEntities.findIndex( - (element) => element.urn === entity.urn && element.type === entity.type, - ) < 0, - ); - setSelectedEntities(Array.from(new Set(selectedEntities.concat(entitiesToAdd)))); - } else { - // Filter out the current page of entity urns from the list - setSelectedEntities(selectedEntities.filter((entity) => searchResultUrns.indexOf(entity.urn) === -1)); - } - }; - - useEffect(() => { - if (!isSelectMode) { - setSelectedEntities([]); - } - }, [isSelectMode]); - - useEffect(() => { - if (defaultFilters && filters.length === 0) { - onChangeFilters(defaultFilters); - } - // only want to run once on page load - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - /** - * Compute the final Facet fields that we show in the left hand search Filters (aggregation). - * - * Do this by filtering out any fields that are included in the fixed filters. - */ - const finalFacets = - (fixedFilters && removeFixedFiltersFromFacets(fixedFilters, data?.facets || [])) || data?.facets; - - // used for logging impact anlaysis events - const degreeFilter = filters.find((filter) => filter.field === DEGREE_FILTER_NAME); - - // we already have some lineage logging through Tab events, but this adds additional context, particularly degree - if (!loading && (degreeFilter?.values?.length || 0) > 0) { - analytics.event({ - type: EventType.SearchAcrossLineageResultsViewEvent, - query, - page, - total: data?.total || 0, - maxDegree: degreeFilter?.values?.sort()?.reverse()[0] || '1', - }); - } - - let errorMessage = ''; - if (error) { - errorMessage = - 'Failed to load results! An unexpected error occurred. This may be due to a timeout when fetching lineage results.'; - } - - return ( - - {error && } - {showFilterBar && ( - onChangeQuery(addFixedQuery(q, fixedQuery as string, emptySearchQuery as string))} - placeholderText={placeholderText} - onToggleFilters={onToggleFilters} - downloadSearchResults={(input) => refetchForDownload(input)} - filters={finalFilters} - query={finalQuery} - isSelectMode={isSelectMode} - isSelectAll={selectedEntities.length > 0 && isListSubset(searchResultUrns, selectedEntityUrns)} - setIsSelectMode={setIsSelectMode} - selectedEntities={selectedEntities} - onChangeSelectAll={onChangeSelectAll} - refetch={() => refetch({ input: searchInput })} - searchBarStyle={searchBarStyle} - searchBarInputStyle={searchBarInputStyle} - numResults={data?.total} - /> - )} - - - ); -}; diff --git a/datahub-web-react/src/app/homeV2/layout/NavLinksMenu.tsx b/datahub-web-react/src/app/homeV2/layout/NavLinksMenu.tsx deleted file mode 100644 index f5cf8e4e610ef..0000000000000 --- a/datahub-web-react/src/app/homeV2/layout/NavLinksMenu.tsx +++ /dev/null @@ -1,365 +0,0 @@ -import React, { useState } from 'react'; -import styled, { useTheme } from 'styled-components/macro'; - -import { Link } from 'react-router-dom'; -import { Tooltip } from 'antd'; - -import { useAppConfig } from '../../useAppConfig'; -import { HOME_PAGE_INGESTION_ID } from '../../onboarding/config/HomePageOnboardingConfig'; -import { useUserContext } from '../../context/useUserContext'; -import { HelpLinkRoutes, PageRoutes } from '../../../conf/Global'; -import { useUpdateEducationStepsAllowList } from '../../onboarding/useUpdateEducationStepsAllowList'; - -import InboxMenuIcon from '../../../images/inboxMenuIcon.svg?react'; -import AnalyticsMenuIcon from '../../../images/analyticsMenuIcon.svg?react'; -import GovernMenuIcon from '../../../images/governMenuIcon.svg?react'; -import ObserveMenuIcon from '../../../images/observeMenuIcon.svg?react'; -import IngestionMenuIcon from '../../../images/ingestionMenuIcon.svg?react'; -import SettingsMenuIcon from '../../../images/settingsMenuIcon.svg?react'; -import HelpMenuIcon from '../../../images/help-icon.svg?react'; -import { useGlobalSettingsContext } from '../../context/GlobalSettings/GlobalSettingsContext'; -import CustomNavLink from './CustomNavLink'; -import { NavMenuItem, NavSubMenuItem } from './types'; -import { useHandleOnboardingTour } from '../../onboarding/useHandleOnboardingTour'; - -const LinksWrapper = styled.div<{ areLinksHidden?: boolean }>` - opacity: 1; - transition: opacity 0.5s; - - ${(props) => - props.areLinksHidden && - ` - opacity: 0; - width: 0; - `} -`; - -const LinkWrapper = styled.span` - position: relative; - display: flex; - align-items: center; - justify-content: center; - border-radius: 47px; - height: 52px; - width: 52px; - line-height: 0; - box-shadow: 0px 0px 8px 4px rgba(0, 0, 0, 0); - transition: all 200ms ease; - color: #f9fafc; - - &:hover { - cursor: pointer; - background-color: #4b39bc; - box-shadow: 0px 0px 8px 4px rgba(0, 0, 0, 0.15); - } - - & svg { - width: 24px; - height: 24px; - fill: #f9fafc; - } -`; - -// Use to position the submenu -const SubMenu = styled.div` - position: absolute; - top: 3px; - left: 50px; - width: 200px; - padding-left: 10px; -`; - -// Used to style the submenu -const SubMenuContent = styled.div` - border-radius: 12px; - background: rgba(92, 63, 209, 0.95); - box-shadow: 0px 8px 8px 4px rgba(0, 0, 0, 0.25); - padding: 8px; - - & a, - div { - display: block; - border-radius: 12px; - color: #fff; - font: 700 12px/20px Mulish; - padding: 8px 12px; - white-space: break-spaces; - - & span { - display: block; - font: 600 10px/12px Mulish; - } - - &:hover { - background-color: #4b39bc; - } - } -`; - -const SubMenuTitle = styled.div` - border-radius: 12px; - background: #2f2477; - padding: 8px 12px; - font: 700 12px/20px Mulish; - margin-bottom: 4px; -`; - -interface Props { - areLinksHidden?: boolean; -} - -export function NavLinksMenu(props: Props) { - const { areLinksHidden } = props; - const me = useUserContext(); - const { config } = useAppConfig(); - const themeConfig = useTheme(); - const { helpLinkState } = useGlobalSettingsContext(); - const { isEnabled: isHelpLinkEnabled, label, link } = helpLinkState; - const helpMenuLabel = label; - const helpMenuLink = link; - const version = config?.appVersion; - const showAddHelpLink = !isHelpLinkEnabled && me.platformPrivileges?.manageGlobalSettings; - - // Submenu states - const [showGovernMenu, setShowGovernMenu] = useState(false); - const [showObserveMenu, setShowObserveMenu] = useState(false); - const [showHelpMenu, setShowHelpMenu] = useState(false); - - // Flags to show/hide menu items - const isAnalyticsEnabled = config?.analyticsConfig.enabled; - const isIngestionEnabled = config?.managedIngestionConfig.enabled; - const isActionRequestsEnabled = config?.actionRequestsConfig.enabled; - const isTestsEnabled = config?.testsConfig.enabled; - const isAutomationsEnabled = config?.classificationConfig.enabled; - - const showSettings = true; - const showAnalytics = (isAnalyticsEnabled && me && me?.platformPrivileges?.viewAnalytics) || false; - const showIngestion = - isIngestionEnabled && me && me.platformPrivileges?.manageIngestion && me.platformPrivileges?.manageSecrets; - const showActionRequests = isActionRequestsEnabled || false; - const showTests = (isTestsEnabled && me?.platformPrivileges?.manageTests) || false; - const showDatasetHealth = config?.featureFlags?.datasetHealthDashboardEnabled; - const showObserve = showDatasetHealth; - const showDocumentationCenter = config?.featureFlags?.documentationFormsEnabled || false; // TODO: Add platformPrivileges check - - // Update education steps allow list - useUpdateEducationStepsAllowList(!!showIngestion, HOME_PAGE_INGESTION_ID); - - const { showOnboardingTour } = useHandleOnboardingTour(); - - // Help menu options - const HelpContentMenuItems = themeConfig.content.menu.items.map((value) => ({ - title: value.label, - description: value.description || '', - link: value.path || null, - isHidden: false, - target: '_blank', - rel: 'noopener noreferrer', - })) as NavSubMenuItem[]; - - // Menu Items - const menuItems: Array = [ - { - icon: InboxMenuIcon, - title: 'Inbox', - description: 'Review and approve metadata proposals', - link: PageRoutes.ACTION_REQUESTS, - isHidden: !showActionRequests, - }, - { - icon: AnalyticsMenuIcon, - title: 'Analytics', - description: 'Explore data usage and trends', - link: PageRoutes.ANALYTICS, - isHidden: !showAnalytics, - }, - { - icon: GovernMenuIcon, - title: 'Govern', - description: 'Manage data access and quality', - link: null, - subMenu: { - isOpen: showGovernMenu, - open: () => setShowGovernMenu(true), - close: () => setShowGovernMenu(false), - items: [ - { - title: 'Glossary', - description: 'View and modify your business glossary', - link: PageRoutes.GLOSSARY, - isHidden: false, - }, - { - title: 'Domains', - description: 'Manage related groups of data assets', - link: PageRoutes.DOMAINS, - isHidden: false, - }, - { - title: 'Tests', - description: 'Monitor policies & automate actions across data assets', - link: PageRoutes.TESTS, - isHidden: !showTests, - }, - { - title: 'Automations', - description: 'Monitor policies & automate actions across data assets', - link: PageRoutes.AUTOMATIONS, - isHidden: !isAutomationsEnabled, - }, - { - title: 'Documentation', - description: 'Manage your documentation standards', - link: PageRoutes.GOVERN_DASHBOARD, - isHidden: !showDocumentationCenter, - }, - ], - }, - }, - { - icon: ObserveMenuIcon, - title: 'Observe', - description: 'Monitor data health and usage', - link: null, - isHidden: !showObserve, - subMenu: { - isOpen: showObserveMenu, - open: () => setShowObserveMenu(true), - close: () => setShowObserveMenu(false), - items: [ - { - title: 'Dataset Health', - description: - "Monitor active incidents & failing assertions across your organization's datasets", - link: PageRoutes.DATASET_HEALTH_DASHBOARD, - isHidden: !showDatasetHealth, - }, - ], - }, - }, - { - icon: IngestionMenuIcon, - title: 'Ingestion', - description: 'Manage data integrations and pipelines', - link: PageRoutes.INGESTION, - isHidden: !showIngestion, - }, - { - icon: SettingsMenuIcon, - title: 'Settings', - description: 'Manage your account and preferences', - link: PageRoutes.SETTINGS, - isHidden: !showSettings, - }, - { - icon: HelpMenuIcon, - title: 'Help', - description: 'Explore help resources and documentation', - link: null, - isHidden: false, - subMenu: { - isOpen: showHelpMenu, - open: () => setShowHelpMenu(true), - close: () => setShowHelpMenu(false), - items: [ - { - title: helpMenuLabel, - description: '', - link: helpMenuLink, - isHidden: !isHelpLinkEnabled, - target: '_blank', - rel: 'noopener noreferrer', - }, - { - title: 'Product Tour', - description: 'Take a quick tour of this page', - isHidden: false, - rel: 'noopener noreferrer', - onClick: showOnboardingTour, - }, - { - title: 'GraphiQL', - description: 'Explore the GraphQL API', - link: HelpLinkRoutes.GRAPHIQL || null, - isHidden: false, - target: '_blank', - rel: 'noopener noreferrer', - }, - { - title: 'OpenAPI', - description: 'Explore the OpenAPI endpoints', - link: HelpLinkRoutes.OPENAPI, - isHidden: false, - target: '_blank', - rel: 'noopener noreferrer', - }, - ...HelpContentMenuItems, - { - title: version || '', - description: '', - link: null, - isHidden: !version, - }, - { - title: 'Add Custom Help Link', - description: '', - link: PageRoutes.SETTINGS_HELP_LINK, - isHidden: !showAddHelpLink, - }, - ], - }, - }, - ]; - - return ( - - {menuItems.map((menuItem) => { - // If menu is hidden, don't show it - if (menuItem.isHidden) return null; - - // Menu item has sub menu items - const hasSubMenu = menuItem.subMenu?.items && menuItem.subMenu?.items.length > 0; - - // Return a menu item with a submenu - if (hasSubMenu) { - const subMenu = ( - - - {menuItem.title} - {menuItem.subMenu?.items.map((subMenuItem) => { - return ( - - ); - })} - - - ); - - return ( - - {menuItem.icon && } - {menuItem.subMenu?.isOpen && subMenu} - - ); - } - - // Render a single menu item - return ( - - - - {menuItem.icon && } - - - - ); - })} - - ); -}