refactor(ui): Extract searchable page into its own component (perf + ux) (#5318)

This commit is contained in:
John Joyce 2022-07-01 18:08:08 -04:00 committed by GitHub
parent cb54629af4
commit a44167c33d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 130 additions and 192 deletions

View File

@ -1,48 +1,21 @@
import React from 'react';
import { Switch, Route, Redirect } from 'react-router-dom';
import { Switch, Route } from 'react-router-dom';
import { Layout } from 'antd';
import { BrowseResultsPage } from './browse/BrowseResultsPage';
import { EntityPage } from './entity/EntityPage';
import { PageRoutes } from '../conf/Global';
import { useEntityRegistry } from './useEntityRegistry';
import { HomePage } from './home/HomePage';
import { SearchPage } from './search/SearchPage';
import { AnalyticsPage } from './analyticsDashboard/components/AnalyticsPage';
import AppConfigProvider from '../AppConfigProvider';
import { ManageIngestionPage } from './ingest/ManageIngestionPage';
import { ManageDomainsPage } from './domain/ManageDomainsPage';
import BusinessGlossaryPage from './glossary/BusinessGlossaryPage';
import { SettingsPage } from './settings/SettingsPage';
import { NoPageFound } from './shared/NoPageFound';
import { SearchRoutes } from './SearchRoutes';
/**
* Container for all views behind an authentication wall.
*/
export const ProtectedRoutes = (): JSX.Element => {
const entityRegistry = useEntityRegistry();
return (
<AppConfigProvider>
<Layout style={{ height: '100%', width: '100%' }}>
<Layout>
<Switch>
<Route exact path="/" render={() => <HomePage />} />
{entityRegistry.getEntities().map((entity) => (
<Route
key={entity.getPathName()}
path={`/${entity.getPathName()}/:urn`}
render={() => <EntityPage entityType={entity.type} />}
/>
))}
<Route path={PageRoutes.SEARCH_RESULTS} render={() => <SearchPage />} />
<Route path={PageRoutes.BROWSE_RESULTS} render={() => <BrowseResultsPage />} />
<Route path={PageRoutes.ANALYTICS} render={() => <AnalyticsPage />} />
<Route path={PageRoutes.POLICIES} render={() => <Redirect to="/settings/policies" />} />
<Route path={PageRoutes.IDENTITIES} render={() => <Redirect to="/settings/identities" />} />
<Route path={PageRoutes.DOMAINS} render={() => <ManageDomainsPage />} />
<Route path={PageRoutes.INGESTION} render={() => <ManageIngestionPage />} />
<Route path={PageRoutes.SETTINGS} render={() => <SettingsPage />} />
<Route path={PageRoutes.GLOSSARY} render={() => <BusinessGlossaryPage />} />
<Route path="/*" component={NoPageFound} />
<Route path="/*" render={() => <SearchRoutes />} />
</Switch>
</Layout>
</Layout>

View File

@ -2,8 +2,6 @@ import React, { useState } from 'react';
import styled from 'styled-components';
import { Alert, Divider, Input, Select } from 'antd';
import { SearchOutlined } from '@ant-design/icons';
import { SearchablePage } from '../../search/SearchablePage';
import { ChartGroup } from './ChartGroup';
import { useGetAnalyticsChartsQuery, useGetMetadataAnalyticsChartsQuery } from '../../../graphql/analytics.generated';
import { useGetHighlightsQuery } from '../../../graphql/highlights.generated';
@ -85,7 +83,7 @@ export const AnalyticsPage = () => {
});
return (
<SearchablePage>
<>
<HighlightGroup>
{highlightLoading && (
<Message type="loading" content="Loading Highlights..." style={{ marginTop: '10%' }} />
@ -179,6 +177,6 @@ export const AnalyticsPage = () => {
</>
))}
</>
</SearchablePage>
</>
);
};

View File

@ -1,11 +1,10 @@
import { Result } from 'antd';
import React from 'react';
import { SearchablePage } from '../search/SearchablePage';
export const UnauthorizedPage = () => {
return (
<SearchablePage>
<>
<Result status="403" title="Unauthorized" subTitle="Sorry, you are not authorized to access this page." />
</SearchablePage>
</>
);
};

View File

@ -1,6 +1,5 @@
import * as React from 'react';
import { Affix } from 'antd';
import { SearchablePage } from '../search/SearchablePage';
import { LegacyBrowsePath } from './LegacyBrowsePath';
import { useGetBrowsePathsQuery } from '../../graphql/browse.generated';
import { EntityType } from '../../types.generated';
@ -26,7 +25,7 @@ export const BrowsableEntityPage = ({
const { data } = useGetBrowsePathsQuery({ variables: { input: { urn: _urn, type: _type } } });
return (
<SearchablePage>
<>
{data && data.browsePaths && data.browsePaths.length > 0 && (
<Affix offsetTop={60}>
<LegacyBrowsePath
@ -39,6 +38,6 @@ export const BrowsableEntityPage = ({
</Affix>
)}
{_children}
</SearchablePage>
</>
);
};

View File

@ -4,7 +4,6 @@ import * as QueryString from 'query-string';
import { Affix, Alert } from 'antd';
import { BrowseCfg } from '../../conf';
import { BrowseResults } from './BrowseResults';
import { SearchablePage } from '../search/SearchablePage';
import { useGetBrowseResultsQuery } from '../../graphql/browse.generated';
import { LegacyBrowsePath } from './LegacyBrowsePath';
import { PageRoutes } from '../../conf/Global';
@ -56,7 +55,7 @@ export const BrowseResultsPage = () => {
}
return (
<SearchablePage>
<>
<Affix offsetTop={60}>
<LegacyBrowsePath type={entityType} path={path} isBrowsable />
</Affix>
@ -74,6 +73,6 @@ export const BrowseResultsPage = () => {
onChangePage={onChangePage}
/>
)}
</SearchablePage>
</>
);
};

View File

@ -1,7 +1,6 @@
import { Typography } from 'antd';
import React from 'react';
import styled from 'styled-components';
import { SearchablePage } from '../search/SearchablePage';
import { DomainsList } from './DomainsList';
const PageContainer = styled.div`
@ -24,18 +23,16 @@ const ListContainer = styled.div``;
export const ManageDomainsPage = () => {
return (
<SearchablePage>
<PageContainer>
<PageHeaderContainer>
<PageTitle level={3}>Domains</PageTitle>
<Typography.Paragraph type="secondary">
View your DataHub Domains. Take administrative actions.
</Typography.Paragraph>
</PageHeaderContainer>
<ListContainer>
<DomainsList />
</ListContainer>
</PageContainer>
</SearchablePage>
<PageContainer>
<PageHeaderContainer>
<PageTitle level={3}>Domains</PageTitle>
<Typography.Paragraph type="secondary">
View your DataHub Domains. Take administrative actions.
</Typography.Paragraph>
</PageHeaderContainer>
<ListContainer>
<DomainsList />
</ListContainer>
</PageContainer>
);
};

View File

@ -5,7 +5,6 @@ import { EntityType } from '../../types.generated';
import { BrowsableEntityPage } from '../browse/BrowsableEntityPage';
import LineageExplorer from '../lineage/LineageExplorer';
import useIsLineageMode from '../lineage/utils/useIsLineageMode';
import { SearchablePage } from '../search/SearchablePage';
import { useEntityRegistry } from '../useEntityRegistry';
import analytics, { EventType } from '../analytics';
import { decodeUrn } from './shared/utils';
@ -32,7 +31,6 @@ export const EntityPage = ({ entityType }: Props) => {
const entity = entityRegistry.getEntity(entityType);
const isBrowsable = entity.isBrowseEnabled();
const isLineageSupported = entity.isLineageEnabled();
const ContainerPage = isBrowsable || isLineageSupported ? BrowsableEntityPage : SearchablePage;
const isLineageMode = useIsLineageMode();
const authenticatedUserUrn = useGetAuthenticatedUserUrn();
const { loading, error, data } = useGetGrantedPrivilegesQuery({
@ -74,8 +72,8 @@ export const EntityPage = ({ entityType }: Props) => {
{error && <Alert type="error" message={error?.message || `Failed to fetch privileges for user`} />}
{data && !canViewEntityPage && <UnauthorizedPage />}
{canViewEntityPage &&
((showNewPage && <SearchablePage>{entityRegistry.renderProfile(entityType, urn)}</SearchablePage>) || (
<ContainerPage
((showNewPage && <>{entityRegistry.renderProfile(entityType, urn)}</>) || (
<BrowsableEntityPage
isBrowsable={isBrowsable}
urn={urn}
type={entityType}
@ -86,7 +84,7 @@ export const EntityPage = ({ entityType }: Props) => {
) : (
entityRegistry.renderProfile(entityType, urn)
)}
</ContainerPage>
</BrowsableEntityPage>
))}
</>
);

View File

@ -5,7 +5,6 @@ import styled from 'styled-components/macro';
import { useGetRootGlossaryNodesQuery, useGetRootGlossaryTermsQuery } from '../../graphql/glossary.generated';
import TabToolbar from '../entity/shared/components/styled/TabToolbar';
import { SearchablePage } from '../search/SearchablePage';
import GlossaryEntitiesPath from './GlossaryEntitiesPath';
import GlossaryEntitiesList from './GlossaryEntitiesList';
import GlossaryBrowser from './GlossaryBrowser/GlossaryBrowser';
@ -55,39 +54,37 @@ function BusinessGlossaryPage() {
return (
<>
<SearchablePage>
<GlossaryWrapper>
<BrowserWrapper width={browserWidth}>
<GlossarySearch />
<GlossaryBrowser rootNodes={nodes} rootTerms={terms} />
</BrowserWrapper>
<ProfileSidebarResizer
setSidePanelWidth={(width) =>
setBrowserWidth(Math.min(Math.max(width, MIN_BROWSWER_WIDTH), MAX_BROWSER_WIDTH))
}
initialSize={browserWidth}
isSidebarOnLeft
/>
<MainContentWrapper>
<GlossaryEntitiesPath />
<HeaderWrapper>
<Typography.Title level={3}>Glossary</Typography.Title>
<div>
<Button type="text" onClick={() => setIsCreateTermModalVisible(true)}>
<PlusOutlined /> Add Term
</Button>
<Button type="text" onClick={() => setIsCreateNodeModalVisible(true)}>
<PlusOutlined /> Add Term Group
</Button>
</div>
</HeaderWrapper>
{hasTermsOrNodes && <GlossaryEntitiesList nodes={nodes || []} terms={terms || []} />}
{!hasTermsOrNodes && (
<EmptyGlossarySection refetchForTerms={refetchForTerms} refetchForNodes={refetchForNodes} />
)}
</MainContentWrapper>
</GlossaryWrapper>
</SearchablePage>
<GlossaryWrapper>
<BrowserWrapper width={browserWidth}>
<GlossarySearch />
<GlossaryBrowser rootNodes={nodes} rootTerms={terms} />
</BrowserWrapper>
<ProfileSidebarResizer
setSidePanelWidth={(width) =>
setBrowserWidth(Math.min(Math.max(width, MIN_BROWSWER_WIDTH), MAX_BROWSER_WIDTH))
}
initialSize={browserWidth}
isSidebarOnLeft
/>
<MainContentWrapper>
<GlossaryEntitiesPath />
<HeaderWrapper>
<Typography.Title level={3}>Glossary</Typography.Title>
<div>
<Button type="text" onClick={() => setIsCreateTermModalVisible(true)}>
<PlusOutlined /> Add Term
</Button>
<Button type="text" onClick={() => setIsCreateNodeModalVisible(true)}>
<PlusOutlined /> Add Term Group
</Button>
</div>
</HeaderWrapper>
{hasTermsOrNodes && <GlossaryEntitiesList nodes={nodes || []} terms={terms || []} />}
{!hasTermsOrNodes && (
<EmptyGlossarySection refetchForTerms={refetchForTerms} refetchForNodes={refetchForNodes} />
)}
</MainContentWrapper>
</GlossaryWrapper>
{isCreateTermModalVisible && (
<CreateGlossaryEntityModal
entityType={EntityType.GlossaryTerm}

View File

@ -1,7 +1,6 @@
import { Tabs, Typography } from 'antd';
import React, { useState } from 'react';
import styled from 'styled-components';
import { SearchablePage } from '../search/SearchablePage';
import { IngestionSourceList } from './source/IngestionSourceList';
import { SecretsList } from './secret/SecretsList';
@ -51,22 +50,18 @@ export const ManageIngestionPage = () => {
};
return (
<SearchablePage>
<PageContainer>
<PageHeaderContainer>
<PageTitle level={3}>Manage Ingestion</PageTitle>
<Typography.Paragraph type="secondary">
Create, schedule, and run DataHub ingestion pipelines.
</Typography.Paragraph>
</PageHeaderContainer>
<StyledTabs activeKey={selectedTab} size="large" onTabClick={(tab: string) => onClickTab(tab)}>
<Tab key={TabType.Sources} tab={TabType.Sources} />
<Tab key={TabType.Secrets} tab={TabType.Secrets} />
</StyledTabs>
<ListContainer>
{selectedTab === TabType.Sources ? <IngestionSourceList /> : <SecretsList />}
</ListContainer>
</PageContainer>
</SearchablePage>
<PageContainer>
<PageHeaderContainer>
<PageTitle level={3}>Manage Ingestion</PageTitle>
<Typography.Paragraph type="secondary">
Create, schedule, and run DataHub ingestion pipelines.
</Typography.Paragraph>
</PageHeaderContainer>
<StyledTabs activeKey={selectedTab} size="large" onTabClick={(tab: string) => onClickTab(tab)}>
<Tab key={TabType.Sources} tab={TabType.Sources} />
<Tab key={TabType.Secrets} tab={TabType.Secrets} />
</StyledTabs>
<ListContainer>{selectedTab === TabType.Sources ? <IngestionSourceList /> : <SecretsList />}</ListContainer>
</PageContainer>
);
};

View File

@ -2,8 +2,6 @@ import React, { useEffect, useState } from 'react';
import * as QueryString from 'query-string';
import { useHistory, useLocation, useParams } from 'react-router';
import { Alert } from 'antd';
import { SearchablePage } from './SearchablePage';
import { useEntityRegistry } from '../useEntityRegistry';
import { FacetFilterInput, EntityType } from '../../types.generated';
import useFilters from './utils/useFilters';
@ -81,20 +79,6 @@ export const SearchPage = () => {
}
}, [query, data, loading]);
const onSearch = (q: string, type?: EntityType) => {
if (q.trim().length === 0) {
return;
}
analytics.event({
type: EventType.SearchEvent,
query: q,
entityTypeFilter: activeType,
pageNumber: 1,
originPath: window.location.pathname,
});
navigateToSearchUrl({ type: type || activeType, query: q, page: 1, history });
};
const onChangeFilters = (newFilters: Array<FacetFilterInput>) => {
navigateToSearchUrl({ type: activeType, query, page: 1, filters: newFilters, history });
};
@ -104,7 +88,7 @@ export const SearchPage = () => {
};
return (
<SearchablePage initialQuery={query} onSearch={onSearch}>
<>
{!loading && error && (
<Alert type="error" message={error?.message || `Search failed to load for query ${query}`} />
)}
@ -123,6 +107,6 @@ export const SearchPage = () => {
numResultsPerPage={numResultsPerPage}
setNumResultsPerPage={setNumResultsPerPage}
/>
</SearchablePage>
</>
);
};

View File

@ -1,7 +1,7 @@
import React, { useEffect } from 'react';
import { useHistory } from 'react-router';
import { useHistory, useLocation } from 'react-router';
import * as QueryString from 'query-string';
import { useTheme } from 'styled-components';
import { SearchHeader } from './SearchHeader';
import { useEntityRegistry } from '../useEntityRegistry';
import { EntityType } from '../../types.generated';
@ -21,13 +21,11 @@ const styles = {
};
interface Props extends React.PropsWithChildren<any> {
initialQuery?: string;
onSearch?: (query: string, type?: EntityType) => void;
onAutoComplete?: (query: string) => void;
}
const defaultProps = {
initialQuery: '',
onSearch: undefined,
onAutoComplete: undefined,
};
@ -35,7 +33,11 @@ const defaultProps = {
/**
* A page that includes a sticky search header (nav bar)
*/
export const SearchablePage = ({ initialQuery, onSearch, onAutoComplete, children }: Props) => {
export const SearchablePage = ({ onSearch, onAutoComplete, children }: Props) => {
const location = useLocation();
const params = QueryString.parse(location.search, { arrayFormat: 'comma' });
const currentQuery: string = decodeURIComponent(params.query ? (params.query as string) : '');
const history = useHistory();
const entityRegistry = useEntityRegistry();
const themeConfig = useTheme();
@ -75,21 +77,21 @@ export const SearchablePage = ({ initialQuery, onSearch, onAutoComplete, childre
// Load correct autocomplete results on initial page load.
useEffect(() => {
if (initialQuery && initialQuery.trim() !== '') {
if (currentQuery && currentQuery.trim() !== '') {
getAutoCompleteResults({
variables: {
input: {
query: initialQuery,
query: currentQuery,
},
},
});
}
}, [initialQuery, getAutoCompleteResults]);
}, [currentQuery, getAutoCompleteResults]);
return (
<>
<SearchHeader
initialQuery={initialQuery as string}
initialQuery={currentQuery as string}
placeholderText={themeConfig.content.search.searchbarMessage}
suggestions={
(suggestionsData &&

View File

@ -6,7 +6,6 @@ import styled from 'styled-components';
import { ANTD_GRAY } from '../entity/shared/constants';
import { ManageIdentities } from '../identity/ManageIdentities';
import { ManagePolicies } from '../policy/ManagePolicies';
import { SearchablePage } from '../search/SearchablePage';
import { useAppConfig } from '../useAppConfig';
import { useGetAuthenticatedUser } from '../useGetAuthenticatedUser';
import { AccessTokens } from './AccessTokens';
@ -78,56 +77,54 @@ export const SettingsPage = () => {
const showUsersGroups = (isIdentityManagementEnabled && me && me.platformPrivileges.manageIdentities) || false;
return (
<SearchablePage>
<PageContainer>
<SettingsBarContainer>
<SettingsBarHeader>
<PageTitle level={3}>Settings</PageTitle>
<Typography.Paragraph type="secondary">Manage your DataHub settings.</Typography.Paragraph>
</SettingsBarHeader>
<ThinDivider />
<Menu
selectable={false}
mode="inline"
style={{ width: 256, marginTop: 8 }}
selectedKeys={[activePath]}
onClick={(newPath) => {
history.push(`${url}/${newPath.key}`);
}}
>
<Menu.ItemGroup title="Developer">
<Menu.Item key="tokens">
<SafetyCertificateOutlined />
<ItemTitle>Access Tokens</ItemTitle>
</Menu.Item>
<PageContainer>
<SettingsBarContainer>
<SettingsBarHeader>
<PageTitle level={3}>Settings</PageTitle>
<Typography.Paragraph type="secondary">Manage your DataHub settings.</Typography.Paragraph>
</SettingsBarHeader>
<ThinDivider />
<Menu
selectable={false}
mode="inline"
style={{ width: 256, marginTop: 8 }}
selectedKeys={[activePath]}
onClick={(newPath) => {
history.push(`${url}/${newPath.key}`);
}}
>
<Menu.ItemGroup title="Developer">
<Menu.Item key="tokens">
<SafetyCertificateOutlined />
<ItemTitle>Access Tokens</ItemTitle>
</Menu.Item>
</Menu.ItemGroup>
{(showPolicies || showUsersGroups) && (
<Menu.ItemGroup title="Access">
{showPolicies && (
<Menu.Item key="identities">
<UsergroupAddOutlined />
<ItemTitle>Users & Groups</ItemTitle>
</Menu.Item>
)}
{showUsersGroups && (
<Menu.Item key="policies">
<BankOutlined />
<ItemTitle>Privileges</ItemTitle>
</Menu.Item>
)}
</Menu.ItemGroup>
{(showPolicies || showUsersGroups) && (
<Menu.ItemGroup title="Access">
{showPolicies && (
<Menu.Item key="identities">
<UsergroupAddOutlined />
<ItemTitle>Users & Groups</ItemTitle>
</Menu.Item>
)}
{showUsersGroups && (
<Menu.Item key="policies">
<BankOutlined />
<ItemTitle>Privileges</ItemTitle>
</Menu.Item>
)}
</Menu.ItemGroup>
)}
</Menu>
</SettingsBarContainer>
<Switch>
<Route exact path={path}>
<Redirect to={`${pathname}${pathname.endsWith('/') ? '' : '/'}${DEFAULT_PATH.path}`} />
</Route>
{PATHS.map((p) => (
<Route path={`${path}/${p.path.replace('/', '')}`} render={() => p.content} key={p.path} />
))}
</Switch>
</PageContainer>
</SearchablePage>
)}
</Menu>
</SettingsBarContainer>
<Switch>
<Route exact path={path}>
<Redirect to={`${pathname}${pathname.endsWith('/') ? '' : '/'}${DEFAULT_PATH.path}`} />
</Route>
{PATHS.map((p) => (
<Route path={`${path}/${p.path.replace('/', '')}`} render={() => p.content} key={p.path} />
))}
</Switch>
</PageContainer>
);
};