Skip to content

Commit

Permalink
RHCLOUD-28295 - update chrome to use autosuggest search API
Browse files Browse the repository at this point in the history
  • Loading branch information
florkbr committed Oct 9, 2023
1 parent 6d9c5d8 commit 3df1d80
Show file tree
Hide file tree
Showing 7 changed files with 73 additions and 76 deletions.
6 changes: 2 additions & 4 deletions src/components/Search/SearchDescription.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
import React from 'react';
import { Text, TextContent } from '@patternfly/react-core/dist/dynamic/components/Text';
import parseHighlights from './parseHighlight';

import './SearchDescription.scss';

const SearchDescription = ({ description, highlight = [] }: { highlight?: string[]; description: string }) => {
const parsedDescription = parseHighlights(description, highlight);
const SearchDescription = ({ description }: { description: string }) => {
return (
<TextContent>
<Text
component="small"
className="chr-c-search__item__description pf-v5-u-color-100 pf-v5-u-text-break-word"
dangerouslySetInnerHTML={{ __html: parsedDescription }}
dangerouslySetInnerHTML={{ __html: description }}
></Text>
</TextContent>
);
Expand Down
27 changes: 15 additions & 12 deletions src/components/Search/SearchGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,24 @@ import React from 'react';
import ChromeLink from '../ChromeLink';
import SearchDescription from './SearchDescription';
import SearchTitle from './SearchTitle';
import { HighlightingResponseType, SearchResultItem } from './SearchTypes';
import { AUTOSUGGEST_TERM_DELIMITER, SearchResultItem } from './SearchTypes';

const SearchGroup = ({ items, highlighting }: { items: SearchResultItem[]; highlighting: HighlightingResponseType }) => {
const SearchGroup = ({ items }: { items: SearchResultItem[] }) => {
return items.length > 0 ? (
<MenuGroup>
{items.map(({ id, allTitle, bundle_title, abstract, relative_uri }) => (
<MenuItem
className="pf-v5-u-mb-xs"
component={(props) => <ChromeLink {...props} href={relative_uri} />}
description={<SearchDescription highlight={highlighting[id]?.abstract} description={abstract} />}
key={id}
>
<SearchTitle title={allTitle} bundleTitle={bundle_title?.[0]} />
</MenuItem>
))}
{items.map(({ term, payload }) => {
const [allTitle, bundle_title, abstract] = term.split(AUTOSUGGEST_TERM_DELIMITER);
return (
<MenuItem
className="pf-v5-u-mb-xs"
component={(props) => <ChromeLink {...props} href={payload} />}
description={<SearchDescription description={abstract} />}
key={crypto.randomUUID()}
>
<SearchTitle title={allTitle} bundleTitle={bundle_title?.[0]} />
</MenuItem>
);
})}
</MenuGroup>
) : null;
};
Expand Down
3 changes: 3 additions & 0 deletions src/components/Search/SearchInput.scss
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@
display: none;
}
}
small {
display: inline-block;
}
}
&__empty-state {
.pf-v5-c-empty-state__icon {
Expand Down
67 changes: 32 additions & 35 deletions src/components/Search/SearchInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import debounce from 'lodash/debounce';

import './SearchInput.scss';
import SearchGroup from './SearchGroup';
import { HighlightingResponseType, SearchResponseType, SearchResultItem } from './SearchTypes';
import { AUTOSUGGEST_HIGHLIGHT_TAG, AUTOSUGGEST_TERM_DELIMITER, SearchResponseType, SearchResultItem } from './SearchTypes';
import EmptySearchState from './EmptySearchState';
import { isProd } from '../../utils/common';
import { useSegment } from '../../analytics/useSegment';
Expand All @@ -36,22 +36,20 @@ const FUZZY_RANGE_TAG = 'FUZZY_RANGE_TAG';
*/

const BASE_SEARCH = new URLSearchParams();
BASE_SEARCH.append(
'q',
`${REPLACE_TAG} OR *${REPLACE_TAG}~${FUZZY_RANGE_TAG} OR ${REPLACE_TAG}*~${FUZZY_RANGE_TAG} OR ${REPLACE_TAG}~${FUZZY_RANGE_TAG}`
); // add query replacement tag and enable fuzzy search with ~ and wildcards
BASE_SEARCH.append('fq', 'documentKind:ModuleDefinition'); // search for ModuleDefinition documents
BASE_SEARCH.append('rows', '10'); // request 10 results
BASE_SEARCH.append('hl', 'true'); // enable highlight
BASE_SEARCH.append('hl.method', 'original'); // choose highlight method
BASE_SEARCH.append('hl.fl', 'abstract'); // highlight description
BASE_SEARCH.append('hl.fl', 'allTitle'); // highlight title
BASE_SEARCH.append('hl.fl', 'bundle_title'); // highlight bundle title
BASE_SEARCH.append('hl.fl', 'bundle'); // highlight bundle id
BASE_SEARCH.append('hl.snippets', '3'); // enable up to 3 highlights in a single string
BASE_SEARCH.append('hl.mergeContiguous', 'true'); // Use only one highlight attribute to simply tag replacement.

const BASE_URL = new URL(`https://access.${IS_PROD ? '' : 'stage.'}redhat.com/hydra/rest/search/platform/console/`);
BASE_SEARCH.append('redhat_client', 'console'); // required client id
BASE_SEARCH.append('q', REPLACE_TAG); // add query replacement tag and enable fuzzy search with ~ and wildcards
// BASE_SEARCH.append('fq', 'documentKind:ModuleDefinition'); // search for ModuleDefinition documents
BASE_SEARCH.append('suggest.count', '10'); // request 10 results
// BASE_SEARCH.append('hl', 'true'); // enable highlight
// BASE_SEARCH.append('hl.method', 'original'); // choose highlight method
// BASE_SEARCH.append('hl.fl', 'abstract'); // highlight description
// BASE_SEARCH.append('hl.fl', 'allTitle'); // highlight title
// BASE_SEARCH.append('hl.fl', 'bundle_title'); // highlight bundle title
// BASE_SEARCH.append('hl.fl', 'bundle'); // highlight bundle id
// BASE_SEARCH.append('hl.snippets', '3'); // enable up to 3 highlights in a single string
// BASE_SEARCH.append('hl.mergeContiguous', 'true'); // Use only one highlight attribute to simply tag replacement.

const BASE_URL = new URL(`https://access.${IS_PROD ? '' : 'stage.'}redhat.com/hydra/proxy/gss-diag/rs/search/autosuggest`);
// search API stopped receiving encoded search string
BASE_URL.search = decodeURIComponent(BASE_SEARCH.toString());
const SEARCH_QUERY = BASE_URL.toString();
Expand All @@ -74,10 +72,9 @@ type SearchCategories = {
};

const initialSearchState: SearchResponseType = {
docs: [],
maxScore: 0,
numFound: 0,
start: 0,
suggest: {
default: {},
},
};

type SearchInputListener = {
Expand All @@ -89,7 +86,6 @@ const SearchInput = ({ onStateChange }: SearchInputListener) => {
const [searchValue, setSearchValue] = useState('');
const [isFetching, setIsFetching] = useState(false);
const [searchResults, setSearchResults] = useState<SearchResponseType>(initialSearchState);
const [highlighting, setHighlighting] = useState<HighlightingResponseType>({});
const { ready, analytics } = useSegment();
const blockCloseEvent = useRef(false);

Expand All @@ -99,19 +95,22 @@ const SearchInput = ({ onStateChange }: SearchInputListener) => {
const containerRef = useRef<HTMLDivElement>(null);
const { md } = useWindowWidth();

const resultCount = searchResults?.suggest?.default[searchValue]?.numFound || 0;

// sort result items based on matched field and its priority
const resultCategories = useMemo(
() =>
searchResults.docs.reduce<SearchCategories>(
(searchResults?.suggest?.default[searchValue]?.suggestions || []).reduce<SearchCategories>(
(acc, curr) => {
if (highlighting[curr.id]?.allTitle) {
const [allTitle, , abstract] = curr.term.split(AUTOSUGGEST_TERM_DELIMITER);
if (allTitle.includes(AUTOSUGGEST_HIGHLIGHT_TAG)) {
return {
...acc,
highLevel: [...acc.highLevel, curr],
};
}

if (highlighting[curr.id]?.abstract) {
if (abstract.includes(AUTOSUGGEST_HIGHLIGHT_TAG)) {
return {
...acc,
midLevel: [...acc.midLevel, curr],
Expand All @@ -129,7 +128,7 @@ const SearchInput = ({ onStateChange }: SearchInputListener) => {
lowLevel: [],
}
),
[searchResults.docs, highlighting]
[searchResults, searchValue]
);

const handleMenuKeys = (event: KeyboardEvent) => {
Expand All @@ -155,7 +154,7 @@ const SearchInput = ({ onStateChange }: SearchInputListener) => {
};

const onInputClick: SearchInputProps['onClick'] = () => {
if (!isOpen && searchResults.numFound > 0) {
if (!isOpen && resultCount > 0) {
if (!md && isExpanded && searchValue !== '') {
setIsOpen(true);
onStateChange(true);
Expand Down Expand Up @@ -216,10 +215,9 @@ const SearchInput = ({ onStateChange }: SearchInputListener) => {
const handleFetch = (value = '') => {
return fetch(SEARCH_QUERY.replaceAll(REPLACE_TAG, value).replaceAll(FUZZY_RANGE_TAG, value.length > 3 ? '2' : '1'))
.then((r) => r.json())
.then(({ response, highlighting }: { highlighting: HighlightingResponseType; response: SearchResponseType }) => {
.then((response: SearchResponseType) => {
if (isMounted.current) {
setSearchResults(response);
setHighlighting(highlighting);
// make sure to calculate resize when switching from loading to sucess state
handleWindowResize();
}
Expand Down Expand Up @@ -280,7 +278,6 @@ const SearchInput = ({ onStateChange }: SearchInputListener) => {
className={isExpanded ? 'pf-u-flex-grow-1' : 'chr-c-search__collapsed'}
/>
);

const menu = (
<Menu ref={menuRef} className="pf-v5-u-pt-sm pf-v5-u-px-md chr-c-search__menu">
<MenuContent>
Expand All @@ -291,14 +288,14 @@ const SearchInput = ({ onStateChange }: SearchInputListener) => {
</Bullseye>
) : (
<>
<MenuGroup label={searchResults.numFound > 0 ? `Top ${searchResults.docs.length} results` : undefined}>
<SearchGroup highlighting={highlighting} items={resultCategories.highLevel} />
<SearchGroup highlighting={highlighting} items={resultCategories.midLevel} />
<SearchGroup highlighting={highlighting} items={resultCategories.lowLevel} />
<MenuGroup label={resultCount > 0 ? `Top ${resultCount} results` : undefined}>
<SearchGroup items={resultCategories.highLevel} />
<SearchGroup items={resultCategories.midLevel} />
<SearchGroup items={resultCategories.lowLevel} />
</MenuGroup>
</>
)}
{searchResults.numFound === 0 && !isFetching && <EmptySearchState />}
{resultCount === 0 && !isFetching && <EmptySearchState />}
</MenuList>
</MenuContent>
</Menu>
Expand Down
13 changes: 8 additions & 5 deletions src/components/Search/SearchTitle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@ import React from 'react';
import { Text, TextContent } from '@patternfly/react-core/dist/dynamic/components/Text';

const SearchTitle = ({ title, bundleTitle }: { title: string; bundleTitle: string }) => {
const showBundleTitle = bundleTitle.replace(/\s/g, '').length > 0;
return (
<TextContent>
<Text component="small" className="pf-v5-u-link-color">
{title}
<span className="pf-v5-u-px-sm">|</span>
{bundleTitle}
</Text>
<Text component="small" className="pf-v5-u-link-color" dangerouslySetInnerHTML={{ __html: title }}></Text>
{showBundleTitle && (
<Text component="small" className="pf-v5-u-link-color">
<span className="pf-v5-u-px-sm">|</span>
</Text>
)}
{showBundleTitle && <Text component="small" className="pf-v5-u-link-color" dangerouslySetInnerHTML={{ __html: bundleTitle }}></Text>}
</TextContent>
);
};
Expand Down
29 changes: 13 additions & 16 deletions src/components/Search/SearchTypes.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,20 @@
export type SearchResultItem = {
abstract: string;
allTitle: string;
bundle: string[];
bundle_title: string[];
documentKind: string;
id: string;
relative_uri: string;
view_uri: string;
term: string;
weight: string;
payload: string;
};

export type SearchResponseType = {
docs: SearchResultItem[];
start: number;
numFound: number;
maxScore: number;
suggest: {
default: {
[recordId: string]: {
numFound: number;
suggestions: SearchResultItem[];
};
};
};
};

export type SearchHighlight = { allTitle?: string[]; abstract?: string[]; bundle_title?: string[]; bundle?: string[] };
export const AUTOSUGGEST_HIGHLIGHT_TAG = '<b>';

export type HighlightingResponseType = {
[recordId: string]: SearchHighlight;
};
export const AUTOSUGGEST_TERM_DELIMITER = '|';
4 changes: 0 additions & 4 deletions src/components/Search/parseHighlight.ts

This file was deleted.

0 comments on commit 3df1d80

Please sign in to comment.