Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(React): Search page UI improvements, 'all' entity search. #2140

Merged
merged 15 commits into from Feb 25, 2021
2 changes: 1 addition & 1 deletion datahub-web-react/src/App.tsx
Expand Up @@ -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 (
Expand Down
94 changes: 94 additions & 0 deletions datahub-web-react/src/app/search/EntityGroupSearchResults.tsx
@@ -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,
jjoyce0510 marked this conversation as resolved.
Show resolved Hide resolved
},
},
});

if (!data?.search?.entities.length) {
return null;
}

const results = data?.search?.entities || [];
jjoyce0510 marked this conversation as resolved.
Show resolved Hide resolved

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 && (
jjoyce0510 marked this conversation as resolved.
Show resolved Hide resolved
<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}
jjoyce0510 marked this conversation as resolved.
Show resolved Hide resolved
renderItem={(item, index) => (
<>
<List.Item>{entityRegistry.renderSearchResult(type, item)}</List.Item>
{index < results.length - 1 && <Divider />}
</>
)}
bordered
/>
</Space>
);
};
145 changes: 145 additions & 0 deletions datahub-web-react/src/app/search/EntitySearchResults.tsx
@@ -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>
);
};
63 changes: 14 additions & 49 deletions datahub-web-react/src/app/search/SearchBar.tsx
@@ -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;
}

Expand All @@ -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>
);
Expand Down
11 changes: 3 additions & 8 deletions datahub-web-react/src/app/search/SearchFilters.tsx
@@ -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';

Expand All @@ -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' }}>
Expand All @@ -53,6 +48,6 @@ export const SearchFilters = ({ facets, selectedFilters, onFilterSelect }: Props
))}
</div>
))}
</Card>
</>
);
};