mirror of
https://github.com/datahub-project/datahub.git
synced 2025-12-28 02:17:53 +00:00
feat(React): Search page UI improvements, 'all' entity search. (#2140)
Co-authored-by: John Joyce <john@acryl.io>
This commit is contained in:
parent
8fe9520ddc
commit
606a5af0c4
@ -43,9 +43,9 @@ const App: React.VFC = () => {
|
||||
const entityRegistry = useMemo(() => {
|
||||
const register = new EntityRegistry();
|
||||
register.register(new DatasetEntity());
|
||||
register.register(new UserEntity());
|
||||
register.register(new DashboardEntity());
|
||||
register.register(new ChartEntity());
|
||||
register.register(new UserEntity());
|
||||
return register;
|
||||
}, []);
|
||||
return (
|
||||
|
||||
@ -0,0 +1,94 @@
|
||||
import { ArrowRightOutlined } from '@ant-design/icons';
|
||||
import { Button, Card, Divider, List, Space, Typography } from 'antd';
|
||||
import * as React from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { SearchCfg } from '../../conf';
|
||||
import { useGetSearchResultsQuery } from '../../graphql/search.generated';
|
||||
import { EntityType } from '../../types.generated';
|
||||
import { IconStyleType } from '../entity/Entity';
|
||||
import { useEntityRegistry } from '../useEntityRegistry';
|
||||
import { navigateToSearchUrl } from './utils/navigateToSearchUrl';
|
||||
|
||||
const styles = {
|
||||
header: { marginBottom: 20 },
|
||||
resultHeaderCardBody: { padding: '16px 24px' },
|
||||
resultHeaderCard: { right: '52px', top: '-40px', position: 'absolute' },
|
||||
resultList: { width: '100%', borderColor: '#f0f0f0', marginTop: '12px', padding: '16px 32px' },
|
||||
seeAllButton: { fontSize: 18 },
|
||||
resultsContainer: { width: '100%', padding: '40px 132px' },
|
||||
};
|
||||
|
||||
interface Props {
|
||||
type: EntityType;
|
||||
query: string;
|
||||
}
|
||||
|
||||
export const EntityGroupSearchResults = ({ type, query }: Props) => {
|
||||
const history = useHistory();
|
||||
const entityRegistry = useEntityRegistry();
|
||||
const { data } = useGetSearchResultsQuery({
|
||||
variables: {
|
||||
input: {
|
||||
type,
|
||||
query,
|
||||
start: 0,
|
||||
count: SearchCfg.RESULTS_PER_PAGE,
|
||||
filters: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!data?.search?.entities.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const results = data?.search?.entities || [];
|
||||
|
||||
return (
|
||||
<Space direction="vertical" style={styles.resultsContainer}>
|
||||
<List
|
||||
header={
|
||||
<span style={styles.header}>
|
||||
<Typography.Title level={3}>{entityRegistry.getCollectionName(type)}</Typography.Title>
|
||||
<Card bodyStyle={styles.resultHeaderCardBody} style={styles.resultHeaderCard as any}>
|
||||
{entityRegistry.getIcon(type, 36, IconStyleType.ACCENT)}
|
||||
</Card>
|
||||
</span>
|
||||
}
|
||||
footer={
|
||||
data?.search &&
|
||||
data?.search?.total > 0 && (
|
||||
<Button
|
||||
type="text"
|
||||
style={styles.seeAllButton}
|
||||
onClick={() =>
|
||||
navigateToSearchUrl({
|
||||
type,
|
||||
query,
|
||||
page: 0,
|
||||
history,
|
||||
entityRegistry,
|
||||
})
|
||||
}
|
||||
>
|
||||
<Typography.Text>
|
||||
See all <b>{entityRegistry.getCollectionName(type)}</b> results
|
||||
</Typography.Text>
|
||||
<ArrowRightOutlined />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
style={styles.resultList}
|
||||
dataSource={results}
|
||||
split={false}
|
||||
renderItem={(item, index) => (
|
||||
<>
|
||||
<List.Item>{entityRegistry.renderSearchResult(type, item)}</List.Item>
|
||||
{index < results.length - 1 && <Divider />}
|
||||
</>
|
||||
)}
|
||||
bordered
|
||||
/>
|
||||
</Space>
|
||||
);
|
||||
};
|
||||
145
datahub-web-react/src/app/search/EntitySearchResults.tsx
Normal file
145
datahub-web-react/src/app/search/EntitySearchResults.tsx
Normal file
@ -0,0 +1,145 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { FilterOutlined } from '@ant-design/icons';
|
||||
import { Alert, Button, Card, Divider, List, Modal, Pagination, Row, Typography } from 'antd';
|
||||
import { SearchCfg } from '../../conf';
|
||||
import { useGetSearchResultsQuery } from '../../graphql/search.generated';
|
||||
import { EntityType, FacetFilterInput } from '../../types.generated';
|
||||
import { IconStyleType } from '../entity/Entity';
|
||||
import { Message } from '../shared/Message';
|
||||
import { useEntityRegistry } from '../useEntityRegistry';
|
||||
import { SearchFilters } from './SearchFilters';
|
||||
|
||||
const styles = {
|
||||
loading: { marginTop: '10%' },
|
||||
addFilters: { backgroundColor: '#F5F5F5' },
|
||||
resultSummary: { color: 'gray', marginTop: '36px' },
|
||||
resultHeaderCardBody: { padding: '16px 24px' },
|
||||
resultHeaderCard: { right: '52px', top: '-40px', position: 'absolute' },
|
||||
resultList: { width: '100%', borderColor: '#f0f0f0', marginTop: '12px', padding: '16px 32px' },
|
||||
paginationRow: { padding: 40 },
|
||||
resultsContainer: { width: '100%', padding: '20px 132px' },
|
||||
};
|
||||
|
||||
interface Props {
|
||||
type: EntityType;
|
||||
query: string;
|
||||
page: number;
|
||||
filters: Array<FacetFilterInput>;
|
||||
onChangeFilters: (filters: Array<FacetFilterInput>) => void;
|
||||
onChangePage: (page: number) => void;
|
||||
}
|
||||
|
||||
export const EntitySearchResults = ({ type, query, page, filters, onChangeFilters, onChangePage }: Props) => {
|
||||
const [isEditingFilters, setIsEditingFilters] = useState(false);
|
||||
const [selectedFilters, setSelectedFilters] = useState(filters);
|
||||
useEffect(() => {
|
||||
setSelectedFilters(filters);
|
||||
}, [filters]);
|
||||
|
||||
const entityRegistry = useEntityRegistry();
|
||||
const { loading, error, data } = useGetSearchResultsQuery({
|
||||
variables: {
|
||||
input: {
|
||||
type,
|
||||
query,
|
||||
start: (page - 1) * SearchCfg.RESULTS_PER_PAGE,
|
||||
count: SearchCfg.RESULTS_PER_PAGE,
|
||||
filters,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const results = data?.search?.entities || [];
|
||||
const pageStart = data?.search?.start || 0;
|
||||
const pageSize = data?.search?.count || 0;
|
||||
const totalResults = data?.search?.total || 0;
|
||||
const lastResultIndex =
|
||||
pageStart * pageSize + pageSize > totalResults ? totalResults : pageStart * pageSize + pageSize;
|
||||
|
||||
const onFilterSelect = (selected: boolean, field: string, value: string) => {
|
||||
const newFilters = selected
|
||||
? [...selectedFilters, { field, value }]
|
||||
: selectedFilters.filter((filter) => filter.field !== field || filter.value !== value);
|
||||
setSelectedFilters(newFilters);
|
||||
};
|
||||
|
||||
const onEditFilters = () => {
|
||||
setIsEditingFilters(true);
|
||||
};
|
||||
|
||||
const onApplyFilters = () => {
|
||||
onChangeFilters(selectedFilters);
|
||||
setIsEditingFilters(false);
|
||||
};
|
||||
|
||||
const onCloseEditFilters = () => {
|
||||
setIsEditingFilters(false);
|
||||
setSelectedFilters(filters);
|
||||
};
|
||||
|
||||
if (error || (!loading && !error && !data)) {
|
||||
return <Alert type="error" message={error?.message || 'Entity failed to load'} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={styles.resultsContainer}>
|
||||
{loading && <Message type="loading" content="Loading..." style={styles.loading} />}
|
||||
<Button style={styles.addFilters} onClick={onEditFilters} data-testid="filters-button">
|
||||
<FilterOutlined />
|
||||
Filters{' '}
|
||||
{filters.length > 0 && (
|
||||
<>
|
||||
{' '}
|
||||
(<b>{filters.length}</b>)
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<Modal
|
||||
title="Filters"
|
||||
footer={<Button onClick={onApplyFilters}>Apply</Button>}
|
||||
visible={isEditingFilters}
|
||||
destroyOnClose
|
||||
onCancel={onCloseEditFilters}
|
||||
>
|
||||
<SearchFilters
|
||||
facets={data?.search?.facets || []}
|
||||
selectedFilters={selectedFilters}
|
||||
onFilterSelect={onFilterSelect}
|
||||
/>
|
||||
</Modal>
|
||||
<Typography.Paragraph style={styles.resultSummary}>
|
||||
Showing{' '}
|
||||
<b>
|
||||
{(page - 1) * pageSize} - {lastResultIndex}
|
||||
</b>{' '}
|
||||
of <b>{totalResults}</b> results
|
||||
</Typography.Paragraph>
|
||||
<List
|
||||
header={
|
||||
<Card bodyStyle={styles.resultHeaderCardBody} style={styles.resultHeaderCard as any}>
|
||||
{entityRegistry.getIcon(type, 36, IconStyleType.ACCENT)}
|
||||
</Card>
|
||||
}
|
||||
style={styles.resultList}
|
||||
dataSource={results}
|
||||
split={false}
|
||||
renderItem={(item, index) => (
|
||||
<>
|
||||
<List.Item>{entityRegistry.renderSearchResult(type, item)}</List.Item>
|
||||
{index < results.length - 1 && <Divider />}
|
||||
</>
|
||||
)}
|
||||
bordered
|
||||
/>
|
||||
<Row justify="center" style={styles.paginationRow}>
|
||||
<Pagination
|
||||
current={page}
|
||||
pageSize={pageSize}
|
||||
total={totalResults}
|
||||
showLessItems
|
||||
onChange={onChangePage}
|
||||
/>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -1,17 +1,18 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Input, AutoComplete, Select } from 'antd';
|
||||
import React from 'react';
|
||||
import { Input, AutoComplete } from 'antd';
|
||||
|
||||
const { Search } = Input;
|
||||
const { Option } = Select;
|
||||
|
||||
const styles = {
|
||||
autoComplete: { width: 650 },
|
||||
};
|
||||
|
||||
interface Props {
|
||||
types: Array<string>;
|
||||
selectedType: string;
|
||||
initialQuery: string;
|
||||
placeholderText: string;
|
||||
suggestions: Array<string>;
|
||||
onSearch: (type: string, query: string) => void;
|
||||
onQueryChange: (type: string, query: string) => void;
|
||||
onSearch: (query: string) => void;
|
||||
onQueryChange: (query: string) => void;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
@ -22,53 +23,17 @@ const defaultProps = {
|
||||
/**
|
||||
* Represents the search bar appearing in the default header view.
|
||||
*/
|
||||
export const SearchBar = ({
|
||||
types,
|
||||
selectedType,
|
||||
initialQuery,
|
||||
placeholderText,
|
||||
suggestions,
|
||||
onSearch,
|
||||
onQueryChange,
|
||||
style,
|
||||
}: Props) => {
|
||||
const [activeType, setActiveType] = useState(selectedType);
|
||||
|
||||
useEffect(() => {
|
||||
setActiveType(selectedType);
|
||||
}, [selectedType]);
|
||||
|
||||
const onTypeChange = (value: string) => {
|
||||
setActiveType(value);
|
||||
};
|
||||
|
||||
export const SearchBar = ({ initialQuery, placeholderText, suggestions, onSearch, onQueryChange, style }: Props) => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
height: '64px',
|
||||
width: '900px',
|
||||
padding: '0px 40px',
|
||||
margin: '0px auto',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
<Select value={activeType} style={{ marginRight: '12px', width: 250 }} onChange={onTypeChange}>
|
||||
{types.map((t) => (
|
||||
<Option key={t} value={t}>
|
||||
{t}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
<div style={style}>
|
||||
<AutoComplete
|
||||
style={{ width: 500 }}
|
||||
style={styles.autoComplete}
|
||||
options={suggestions.map((result: string) => ({ value: result }))}
|
||||
onSelect={(value: string) => onSearch(activeType, value)}
|
||||
onSearch={(value: string) => onQueryChange(activeType, value)}
|
||||
onSelect={(value: string) => onSearch(value)}
|
||||
onSearch={(value: string) => onQueryChange(value)}
|
||||
defaultValue={initialQuery}
|
||||
>
|
||||
<Search placeholder={placeholderText} onSearch={(value: string) => onSearch(activeType, value)} />
|
||||
<Search placeholder={placeholderText} onSearch={(value: string) => onSearch(value)} />
|
||||
</AutoComplete>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
// import { Card } from 'antd';
|
||||
import { Card, Checkbox } from 'antd';
|
||||
import { Checkbox } from 'antd';
|
||||
import { CheckboxChangeEvent } from 'antd/lib/checkbox';
|
||||
import * as React from 'react';
|
||||
|
||||
@ -22,11 +21,7 @@ interface Props {
|
||||
|
||||
export const SearchFilters = ({ facets, selectedFilters, onFilterSelect }: Props) => {
|
||||
return (
|
||||
<Card
|
||||
style={{ border: '1px solid #d2d2d2' }}
|
||||
title={<h3 style={{ marginBottom: '0px' }}>Filters</h3>}
|
||||
bodyStyle={{ padding: '24px 0px' }}
|
||||
>
|
||||
<>
|
||||
{facets.map((facet) => (
|
||||
<div key={facet.field} style={{ padding: '0px 25px 15px 25px' }}>
|
||||
<div style={{ fontWeight: 'bold', marginBottom: '10px' }}>
|
||||
@ -53,6 +48,6 @@ export const SearchFilters = ({ facets, selectedFilters, onFilterSelect }: Props
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import 'antd/dist/antd.css';
|
||||
import { Image, Layout } from 'antd';
|
||||
import { Image, Layout, Space, Typography } from 'antd';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { SearchBar } from './SearchBar';
|
||||
import { ManageAccount } from '../shared/ManageAccount';
|
||||
@ -8,16 +8,32 @@ import { GlobalCfg } from '../../conf';
|
||||
|
||||
const { Header } = Layout;
|
||||
|
||||
const styles = {
|
||||
header: {
|
||||
position: 'fixed',
|
||||
zIndex: 1,
|
||||
width: '100%',
|
||||
backgroundColor: 'rgb(51 62 76)',
|
||||
height: '80px',
|
||||
lineHeight: '20px',
|
||||
color: '#fff',
|
||||
padding: '0px 40px',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
},
|
||||
logoImage: { width: '36px', height: '32px' },
|
||||
title: { color: 'white', paddingLeft: '12px', margin: '0' },
|
||||
};
|
||||
|
||||
type Props = {
|
||||
types: Array<string>;
|
||||
selectedType: string;
|
||||
initialQuery: string;
|
||||
placeholderText: string;
|
||||
suggestions: Array<string>;
|
||||
onSearch: (type: string, query: string) => void;
|
||||
onQueryChange: (type: string, query: string) => void;
|
||||
onSearch: (query: string) => void;
|
||||
onQueryChange: (query: string) => void;
|
||||
authenticatedUserUrn: string;
|
||||
authenticatedUserPictureLink?: string;
|
||||
authenticatedUserPictureLink?: string | null;
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
@ -28,8 +44,6 @@ const defaultProps = {
|
||||
* A header containing a Logo, Search Bar view, & an account management dropdown.
|
||||
*/
|
||||
export const SearchHeader = ({
|
||||
types,
|
||||
selectedType,
|
||||
initialQuery,
|
||||
placeholderText,
|
||||
suggestions,
|
||||
@ -39,43 +53,23 @@ export const SearchHeader = ({
|
||||
authenticatedUserPictureLink,
|
||||
}: Props) => {
|
||||
return (
|
||||
<Header
|
||||
style={{
|
||||
position: 'fixed',
|
||||
zIndex: 1,
|
||||
width: '100%',
|
||||
backgroundColor: 'rgb(51 62 76)',
|
||||
fontSize: '18px',
|
||||
height: '64px',
|
||||
lineHeight: '20px',
|
||||
color: '#fff',
|
||||
padding: '0px 80px',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Link
|
||||
style={{
|
||||
height: '64px',
|
||||
padding: '15px 30px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
to="/"
|
||||
>
|
||||
<Image style={{ width: '34px', height: '30px' }} src={GlobalCfg.LOGO_IMAGE} preview={false} />
|
||||
<div style={{ color: 'white', fontWeight: 'bold', padding: '15px' }}>DataHub</div>
|
||||
</Link>
|
||||
<SearchBar
|
||||
types={types}
|
||||
initialQuery={initialQuery}
|
||||
selectedType={selectedType}
|
||||
placeholderText={placeholderText}
|
||||
suggestions={suggestions}
|
||||
onSearch={onSearch}
|
||||
onQueryChange={onQueryChange}
|
||||
/>
|
||||
<ManageAccount urn={authenticatedUserUrn} pictureLink={authenticatedUserPictureLink} />
|
||||
</div>
|
||||
<Header style={styles.header as any}>
|
||||
<Link to="/">
|
||||
<Space size={4}>
|
||||
<Image style={styles.logoImage} src={GlobalCfg.LOGO_IMAGE} preview={false} />
|
||||
<Typography.Title level={4} style={styles.title}>
|
||||
DataHub
|
||||
</Typography.Title>
|
||||
</Space>
|
||||
</Link>
|
||||
<SearchBar
|
||||
initialQuery={initialQuery}
|
||||
placeholderText={placeholderText}
|
||||
suggestions={suggestions}
|
||||
onSearch={onSearch}
|
||||
onQueryChange={onQueryChange}
|
||||
/>
|
||||
<ManageAccount urn={authenticatedUserUrn} pictureLink={authenticatedUserPictureLink || ''} />
|
||||
</Header>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,18 +1,27 @@
|
||||
import React from 'react';
|
||||
import * as QueryString from 'query-string';
|
||||
import { useHistory, useLocation, useParams } from 'react-router';
|
||||
import { Affix, Col, Row, Tabs, Layout, List, Alert } from 'antd';
|
||||
|
||||
import { Affix, Tabs } from 'antd';
|
||||
import { SearchablePage } from './SearchablePage';
|
||||
import { useGetSearchResultsQuery } from '../../graphql/search.generated';
|
||||
import { SearchResults } from './SearchResults';
|
||||
import { SearchFilters } from './SearchFilters';
|
||||
import { SearchCfg } from '../../conf';
|
||||
import { useEntityRegistry } from '../useEntityRegistry';
|
||||
import { FacetFilterInput } from '../../types.generated';
|
||||
import useFilters from './utils/useFilters';
|
||||
import { navigateToSearchUrl } from './utils/navigateToSearchUrl';
|
||||
import { Message } from '../shared/Message';
|
||||
import { EntitySearchResults } from './EntitySearchResults';
|
||||
import { IconStyleType } from '../entity/Entity';
|
||||
import { EntityGroupSearchResults } from './EntityGroupSearchResults';
|
||||
|
||||
const ALL_ENTITIES_TAB_NAME = 'All';
|
||||
|
||||
const styles = {
|
||||
tabs: {
|
||||
backgroundColor: '#FFFFFF',
|
||||
paddingLeft: '165px',
|
||||
paddingTop: '12px',
|
||||
color: 'rgba(0, 0, 0, 0.45)',
|
||||
},
|
||||
tab: { fontSize: 20 },
|
||||
};
|
||||
|
||||
type SearchPageParams = {
|
||||
type?: string;
|
||||
@ -29,101 +38,73 @@ export const SearchPage = () => {
|
||||
const searchTypes = entityRegistry.getSearchEntityTypes();
|
||||
|
||||
const params = QueryString.parse(location.search, { arrayFormat: 'comma' });
|
||||
const selectedType = entityRegistry.getTypeOrDefaultFromPathName(
|
||||
useParams<SearchPageParams>().type || '',
|
||||
undefined,
|
||||
);
|
||||
const activeType = selectedType || entityRegistry.getDefaultSearchEntityType();
|
||||
const activeType = entityRegistry.getTypeOrDefaultFromPathName(useParams<SearchPageParams>().type || '', undefined);
|
||||
const query: string = params.query ? (params.query as string) : '';
|
||||
const page: number = params.page && Number(params.page as string) > 0 ? Number(params.page as string) : 1;
|
||||
const filters: Array<FacetFilterInput> = useFilters(params);
|
||||
|
||||
const { loading, error, data } = useGetSearchResultsQuery({
|
||||
variables: {
|
||||
input: {
|
||||
type: activeType,
|
||||
query,
|
||||
start: (page - 1) * SearchCfg.RESULTS_PER_PAGE,
|
||||
count: SearchCfg.RESULTS_PER_PAGE,
|
||||
filters,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (error || (!loading && !error && !data)) {
|
||||
return <Alert type="error" message={error?.message || 'Entity failed to load'} />;
|
||||
}
|
||||
|
||||
const onSearchTypeChange = (newType: string) => {
|
||||
const entityType = entityRegistry.getTypeFromCollectionName(newType);
|
||||
navigateToSearchUrl({ type: entityType, query, page: 1, history, entityRegistry });
|
||||
const onSearch = (q: string) => {
|
||||
navigateToSearchUrl({ type: activeType, query: q, page: 1, history, entityRegistry });
|
||||
};
|
||||
|
||||
const onFilterSelect = (selected: boolean, field: string, value: string) => {
|
||||
const newFilters = selected
|
||||
? [...filters, { field, value }]
|
||||
: filters.filter((filter) => filter.field !== field || filter.value !== value);
|
||||
const onChangeSearchType = (newType: string) => {
|
||||
if (newType === ALL_ENTITIES_TAB_NAME) {
|
||||
navigateToSearchUrl({ query, page: 1, history, entityRegistry });
|
||||
} else {
|
||||
const entityType = entityRegistry.getTypeFromCollectionName(newType);
|
||||
navigateToSearchUrl({ type: entityType, query, page: 1, history, entityRegistry });
|
||||
}
|
||||
};
|
||||
|
||||
const onChangeFilters = (newFilters: Array<FacetFilterInput>) => {
|
||||
navigateToSearchUrl({ type: activeType, query, page: 1, filters: newFilters, history, entityRegistry });
|
||||
};
|
||||
|
||||
const onResultsPageChange = (newPage: number) => {
|
||||
const onChangePage = (newPage: number) => {
|
||||
navigateToSearchUrl({ type: activeType, query, page: newPage, filters, history, entityRegistry });
|
||||
};
|
||||
|
||||
const toSearchResults = (elements: any) => (
|
||||
<List
|
||||
dataSource={elements}
|
||||
renderItem={(item) => (
|
||||
<List.Item style={{ padding: 32 }}>{entityRegistry.renderSearchResult(activeType, item)}</List.Item>
|
||||
)}
|
||||
bordered
|
||||
/>
|
||||
);
|
||||
|
||||
const searchResults = toSearchResults(data?.search?.entities || []);
|
||||
|
||||
return (
|
||||
<SearchablePage initialQuery={query} selectedType={selectedType}>
|
||||
<Layout.Content style={{ backgroundColor: 'white' }}>
|
||||
<Affix offsetTop={64}>
|
||||
<Tabs
|
||||
tabBarStyle={{ backgroundColor: 'white', padding: '0px 165px', marginBottom: '0px' }}
|
||||
activeKey={entityRegistry.getCollectionName(activeType)}
|
||||
size="large"
|
||||
onChange={onSearchTypeChange}
|
||||
>
|
||||
{searchTypes.map((t) => (
|
||||
<Tabs.TabPane
|
||||
tab={entityRegistry.getCollectionName(t)}
|
||||
key={entityRegistry.getCollectionName(t)}
|
||||
/>
|
||||
))}
|
||||
</Tabs>
|
||||
</Affix>
|
||||
{loading && <Message type="loading" content="Loading..." style={{ marginTop: '10%' }} />}
|
||||
<Row style={{ width: '80%', margin: 'auto auto', backgroundColor: 'white' }}>
|
||||
<Col style={{ margin: '24px 0px 0px 0px', padding: '0px 16px' }} span={6}>
|
||||
<SearchFilters
|
||||
facets={data?.search?.facets || []}
|
||||
selectedFilters={filters}
|
||||
onFilterSelect={onFilterSelect}
|
||||
<SearchablePage initialQuery={query} onSearch={onSearch}>
|
||||
<Affix offsetTop={80}>
|
||||
<Tabs
|
||||
tabBarStyle={styles.tabs}
|
||||
activeKey={activeType ? entityRegistry.getCollectionName(activeType) : ALL_ENTITIES_TAB_NAME}
|
||||
size="large"
|
||||
onChange={onChangeSearchType}
|
||||
>
|
||||
<Tabs.TabPane
|
||||
style={styles.tab}
|
||||
tab={<span style={styles.tab}>All</span>}
|
||||
key={ALL_ENTITIES_TAB_NAME}
|
||||
/>
|
||||
{searchTypes.map((t) => (
|
||||
<Tabs.TabPane
|
||||
tab={
|
||||
<>
|
||||
{entityRegistry.getIcon(t, 16, IconStyleType.TAB_VIEW)}{' '}
|
||||
<span style={styles.tab}>{entityRegistry.getCollectionName(t)}</span>
|
||||
</>
|
||||
}
|
||||
key={entityRegistry.getCollectionName(t)}
|
||||
/>
|
||||
</Col>
|
||||
<Col style={{ margin: '24px 0px 0px 0px', padding: '0px 16px' }} span={18}>
|
||||
{data?.search && (
|
||||
<SearchResults
|
||||
typeName={entityRegistry.getCollectionName(activeType)}
|
||||
results={searchResults}
|
||||
pageStart={data?.search?.start}
|
||||
pageSize={data.search?.count}
|
||||
totalResults={data.search?.total}
|
||||
onChangePage={onResultsPageChange}
|
||||
/>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
</Layout.Content>
|
||||
))}
|
||||
</Tabs>
|
||||
</Affix>
|
||||
{activeType ? (
|
||||
<EntitySearchResults
|
||||
type={activeType}
|
||||
page={page}
|
||||
query={query}
|
||||
filters={filters}
|
||||
onChangeFilters={onChangeFilters}
|
||||
onChangePage={onChangePage}
|
||||
/>
|
||||
) : (
|
||||
entityRegistry
|
||||
.getSearchEntityTypes()
|
||||
.map((entityType) => <EntityGroupSearchResults type={entityType} query={query} />)
|
||||
)}
|
||||
</SearchablePage>
|
||||
);
|
||||
};
|
||||
|
||||
@ -7,63 +7,48 @@ import { SearchCfg } from '../../conf';
|
||||
import { useEntityRegistry } from '../useEntityRegistry';
|
||||
import { useGetAutoCompleteResultsLazyQuery } from '../../graphql/search.generated';
|
||||
import { navigateToSearchUrl } from './utils/navigateToSearchUrl';
|
||||
import { EntityType } from '../../types.generated';
|
||||
import { useGetAuthenticatedUser } from '../useGetAuthenticatedUser';
|
||||
|
||||
const ALL_ENTITIES_SEARCH_TYPE_NAME = 'All Entities';
|
||||
const styles = {
|
||||
pageContainer: { backgroundColor: '#FFFFFF' },
|
||||
children: { marginTop: 80 },
|
||||
};
|
||||
|
||||
interface Props extends React.PropsWithChildren<any> {
|
||||
selectedType?: EntityType;
|
||||
initialQuery?: string;
|
||||
onSearch?: (query: string) => void;
|
||||
onAutoComplete?: (query: string) => void;
|
||||
}
|
||||
|
||||
const defaultProps = {
|
||||
selectedType: undefined,
|
||||
initialQuery: '',
|
||||
onSearch: undefined,
|
||||
onAutoComplete: undefined,
|
||||
};
|
||||
|
||||
/**
|
||||
* A page that includes a sticky search header (nav bar)
|
||||
*/
|
||||
export const SearchablePage = ({ selectedType, initialQuery, children }: Props) => {
|
||||
export const SearchablePage = ({ initialQuery, onSearch, onAutoComplete, children }: Props) => {
|
||||
const history = useHistory();
|
||||
|
||||
const entityRegistry = useEntityRegistry();
|
||||
const searchTypes = entityRegistry.getSearchEntityTypes();
|
||||
|
||||
const { data: userData } = useGetAuthenticatedUser();
|
||||
|
||||
const searchTypeNames = [
|
||||
ALL_ENTITIES_SEARCH_TYPE_NAME,
|
||||
...searchTypes.map((entityType) => entityRegistry.getCollectionName(entityType)),
|
||||
];
|
||||
|
||||
const selectedSearchTypeName =
|
||||
selectedType && searchTypes.includes(selectedType)
|
||||
? entityRegistry.getCollectionName(selectedType)
|
||||
: ALL_ENTITIES_SEARCH_TYPE_NAME;
|
||||
|
||||
const [getAutoCompleteResults, { data: suggestionsData }] = useGetAutoCompleteResultsLazyQuery();
|
||||
|
||||
const search = (typeName: string, query: string) => {
|
||||
const search = (query: string) => {
|
||||
navigateToSearchUrl({
|
||||
type:
|
||||
ALL_ENTITIES_SEARCH_TYPE_NAME === typeName
|
||||
? undefined
|
||||
: entityRegistry.getTypeFromCollectionName(typeName),
|
||||
query,
|
||||
history,
|
||||
entityRegistry,
|
||||
});
|
||||
};
|
||||
|
||||
const autoComplete = (type: string, query: string) => {
|
||||
const entityType =
|
||||
ALL_ENTITIES_SEARCH_TYPE_NAME === type ? searchTypes[0] : entityRegistry.getTypeFromCollectionName(type);
|
||||
const autoComplete = (query: string) => {
|
||||
getAutoCompleteResults({
|
||||
variables: {
|
||||
input: {
|
||||
type: entityType,
|
||||
type: entityRegistry.getDefaultSearchEntityType(),
|
||||
query,
|
||||
},
|
||||
},
|
||||
@ -71,21 +56,19 @@ export const SearchablePage = ({ selectedType, initialQuery, children }: Props)
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<Layout style={styles.pageContainer}>
|
||||
<SearchHeader
|
||||
types={searchTypeNames}
|
||||
selectedType={selectedSearchTypeName}
|
||||
initialQuery={initialQuery as string}
|
||||
placeholderText={SearchCfg.SEARCH_BAR_PLACEHOLDER_TEXT}
|
||||
suggestions={
|
||||
(suggestionsData && suggestionsData?.autoComplete && suggestionsData.autoComplete.suggestions) || []
|
||||
}
|
||||
onSearch={search}
|
||||
onQueryChange={autoComplete}
|
||||
onSearch={onSearch || search}
|
||||
onQueryChange={onAutoComplete || autoComplete}
|
||||
authenticatedUserUrn={userData?.corpUser?.urn || ''}
|
||||
authenticatedUserPictureLink={userData?.corpUser?.editableInfo?.pictureLink || ''}
|
||||
authenticatedUserPictureLink={userData?.corpUser?.editableInfo?.pictureLink}
|
||||
/>
|
||||
<div style={{ marginTop: 64 }}>{children}</div>
|
||||
<div style={styles.children}>{children}</div>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,42 +1,48 @@
|
||||
import React from 'react';
|
||||
import { fireEvent, render, waitFor } from '@testing-library/react';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { Route } from 'react-router';
|
||||
|
||||
import { SearchPage } from '../SearchPage';
|
||||
import TestPageContainer from '../../../utils/test-utils/TestPageContainer';
|
||||
import { mocks } from '../../../Mocks';
|
||||
import { PageRoutes } from '../../../conf/Global';
|
||||
|
||||
describe('SearchPage', () => {
|
||||
it('renders', () => {
|
||||
render(
|
||||
<MockedProvider mocks={mocks} addTypename={false}>
|
||||
<TestPageContainer initialEntries={['/search/dataset?filter_platform=hive,kafka&page=1&query=sample']}>
|
||||
<SearchPage />
|
||||
<Route path={PageRoutes.SEARCH_RESULTS} render={() => <SearchPage />} />
|
||||
</TestPageContainer>
|
||||
</MockedProvider>,
|
||||
);
|
||||
});
|
||||
|
||||
it('renders loading', () => {
|
||||
it('renders loading', async () => {
|
||||
const { getByText } = render(
|
||||
<MockedProvider mocks={mocks} addTypename={false}>
|
||||
<TestPageContainer initialEntries={['/search/dataset?filter_platform=hive,kafka&page=1&query=sample']}>
|
||||
<SearchPage />
|
||||
<Route path={PageRoutes.SEARCH_RESULTS} render={() => <SearchPage />} />
|
||||
</TestPageContainer>
|
||||
</MockedProvider>,
|
||||
);
|
||||
expect(getByText('Loading...')).toBeInTheDocument();
|
||||
await waitFor(() => expect(getByText('Loading...')).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('renders the selected filters as checked', async () => {
|
||||
const { getByTestId, queryByTestId } = render(
|
||||
<MockedProvider mocks={mocks} addTypename={false}>
|
||||
<TestPageContainer initialEntries={['/search/dataset?filter_platform=kafka&page=1&query=test']}>
|
||||
<SearchPage />
|
||||
<Route path={PageRoutes.SEARCH_RESULTS} render={() => <SearchPage />} />
|
||||
</TestPageContainer>
|
||||
</MockedProvider>,
|
||||
);
|
||||
|
||||
await waitFor(() => expect(getByTestId('filters-button')).toBeInTheDocument());
|
||||
const filtersButton = getByTestId('filters-button');
|
||||
fireEvent.click(filtersButton);
|
||||
|
||||
await waitFor(() => expect(queryByTestId('facet-platform-kafka')).toBeInTheDocument());
|
||||
|
||||
const kafkaPlatformBox = getByTestId('facet-platform-kafka');
|
||||
@ -53,11 +59,15 @@ describe('SearchPage', () => {
|
||||
const { getByTestId, queryByTestId } = render(
|
||||
<MockedProvider mocks={mocks} addTypename={false}>
|
||||
<TestPageContainer initialEntries={['/search/dataset?filter_platform=kafka,hdfs&page=1&query=test']}>
|
||||
<SearchPage />
|
||||
<Route path={PageRoutes.SEARCH_RESULTS} render={() => <SearchPage />} />
|
||||
</TestPageContainer>
|
||||
</MockedProvider>,
|
||||
);
|
||||
|
||||
await waitFor(() => expect(getByTestId('filters-button')).toBeInTheDocument());
|
||||
const filtersButton = getByTestId('filters-button');
|
||||
fireEvent.click(filtersButton);
|
||||
|
||||
await waitFor(() => expect(queryByTestId('facet-platform-kafka')).toBeInTheDocument());
|
||||
|
||||
const kafkaPlatformBox = getByTestId('facet-platform-kafka');
|
||||
@ -71,14 +81,18 @@ describe('SearchPage', () => {
|
||||
});
|
||||
|
||||
it('clicking a filter selects a new filter', async () => {
|
||||
const { getByTestId, queryByTestId, queryByText } = render(
|
||||
const { getByTestId, queryByTestId } = render(
|
||||
<MockedProvider mocks={mocks} addTypename={false}>
|
||||
<TestPageContainer initialEntries={['/search/dataset?filter_platform=kafka&page=1&query=test']}>
|
||||
<SearchPage />
|
||||
<Route path={PageRoutes.SEARCH_RESULTS} render={() => <SearchPage />} />
|
||||
</TestPageContainer>
|
||||
</MockedProvider>,
|
||||
);
|
||||
|
||||
await waitFor(() => expect(getByTestId('filters-button')).toBeInTheDocument());
|
||||
const filtersButton = getByTestId('filters-button');
|
||||
fireEvent.click(filtersButton);
|
||||
|
||||
await waitFor(() => expect(queryByTestId('facet-platform-kafka')).toBeInTheDocument());
|
||||
|
||||
const kafkaPlatformBox = getByTestId('facet-platform-kafka');
|
||||
@ -87,9 +101,7 @@ describe('SearchPage', () => {
|
||||
const hdfsPlatformBox = getByTestId('facet-platform-hdfs');
|
||||
expect(hdfsPlatformBox).toHaveProperty('checked', false);
|
||||
|
||||
expect(queryByText('Loading...')).not.toBeInTheDocument();
|
||||
fireEvent.click(hdfsPlatformBox);
|
||||
expect(queryByText('Loading...')).toBeInTheDocument();
|
||||
|
||||
await waitFor(() => expect(queryByTestId('facet-platform-kafka')).toBeInTheDocument());
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user