Skip to content

Commit

Permalink
Merge pull request #2658 from florkbr/rhcloud-28295
Browse files Browse the repository at this point in the history
RHCLOUD-28295: update chrome to use autosuggest search API
  • Loading branch information
Hyperkid123 authored Nov 2, 2023
2 parents d6f0a38 + eeda7c1 commit e740df9
Show file tree
Hide file tree
Showing 7 changed files with 109 additions and 194 deletions.
69 changes: 0 additions & 69 deletions cypress/e2e/release-gate/search.cy.tsx

This file was deleted.

2 changes: 1 addition & 1 deletion src/components/Search/SearchDescription.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React from 'react';
import { Text, TextContent } from '@patternfly/react-core/dist/dynamic/components/Text';
import parseHighlights from './parseHighlight';

import './SearchDescription.scss';
import parseHighlights from './parseHighlight';

const SearchDescription = ({ description, highlight = [] }: { highlight?: string[]; description: string }) => {
const parsedDescription = parseHighlights(description, highlight);
Expand Down
25 changes: 0 additions & 25 deletions src/components/Search/SearchGroup.tsx

This file was deleted.

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
170 changes: 80 additions & 90 deletions src/components/Search/SearchInput.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,31 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { Bullseye } from '@patternfly/react-core/dist/dynamic/layouts/Bullseye';
import { Menu, MenuContent, MenuGroup, MenuList } from '@patternfly/react-core/dist/dynamic/components/Menu';
import { Menu, MenuContent, MenuGroup, MenuItem, MenuList } from '@patternfly/react-core/dist/dynamic/components/Menu';
import { SearchInput as PFSearchInput, SearchInputProps } from '@patternfly/react-core/dist/dynamic/components/SearchInput';
import { Spinner } from '@patternfly/react-core/dist/dynamic/components/Spinner';
import { Popper } from '@patternfly/react-core/dist/dynamic/helpers/Popper/Popper';

import debounce from 'lodash/debounce';
import uniq from 'lodash/uniq';
import uniqWith from 'lodash/uniqWith';

import './SearchInput.scss';
import SearchGroup from './SearchGroup';
import { HighlightingResponseType, SearchResponseType, SearchResultItem } from './SearchTypes';
import { AUTOSUGGEST_TERM_DELIMITER, SearchAutoSuggestionResponseType, SearchResponseType } from './SearchTypes';
import EmptySearchState from './EmptySearchState';
import { isProd } from '../../utils/common';
import { useSegment } from '../../analytics/useSegment';
import useWindowWidth from '../../hooks/useWindowWidth';
import ChromeLink from '../ChromeLink';
import SearchTitle from './SearchTitle';
import SearchDescription from './SearchDescription';

export type SearchInputprops = {
isExpanded?: boolean;
};

const IS_PROD = isProd();
const REPLACE_TAG = 'REPLACE_TAG';
const FUZZY_RANGE_TAG = 'FUZZY_RANGE_TAG';
const REPLACE_COUNT_TAG = 'REPLACE_COUNT_TAG';
/**
* The ?q is the search term.
* ------
Expand All @@ -36,25 +40,23 @@ 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('q', `alt_titles:${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('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.
BASE_SEARCH.append('rows', `${REPLACE_COUNT_TAG}`); // request 10 results

const BASE_URL = new URL(`https://access.${IS_PROD ? '' : 'stage.'}redhat.com/hydra/rest/search/platform/console/`);
// search API stopped receiving encoded search string
BASE_URL.search = decodeURIComponent(BASE_SEARCH.toString());
const SEARCH_QUERY = BASE_URL.toString();

const SUGGEST_SEARCH = new URLSearchParams();
SUGGEST_SEARCH.append('redhat_client', 'console'); // required client id
SUGGEST_SEARCH.append('q', REPLACE_TAG); // add query replacement tag and enable fuzzy search with ~ and wildcards
SUGGEST_SEARCH.append('suggest.count', '10'); // request 10 results

const SUGGEST_URL = new URL(`https://access.${IS_PROD ? '' : 'stage.'}redhat.com/hydra/proxy/gss-diag/rs/search/autosuggest`);
// search API stopped receiving encoded search string
SUGGEST_URL.search = decodeURIComponent(SUGGEST_SEARCH.toString());
const SUGGEST_SEARCH_QUERY = SUGGEST_URL.toString();

const getMaxMenuHeight = (menuElement?: HTMLDivElement | null) => {
if (!menuElement) {
Expand All @@ -67,17 +69,11 @@ const getMaxMenuHeight = (menuElement?: HTMLDivElement | null) => {
return bodyHeight - menuTopOffset - 4;
};

type SearchCategories = {
highLevel: SearchResultItem[];
midLevel: SearchResultItem[];
lowLevel: SearchResultItem[];
};

const initialSearchState: SearchResponseType = {
docs: [],
maxScore: 0,
numFound: 0,
start: 0,
type SearchItem = {
title: string;
bundleTitle: string;
description: string;
pathname: string;
};

type SearchInputListener = {
Expand All @@ -88,8 +84,7 @@ const SearchInput = ({ onStateChange }: SearchInputListener) => {
const [isOpen, setIsOpen] = useState(false);
const [searchValue, setSearchValue] = useState('');
const [isFetching, setIsFetching] = useState(false);
const [searchResults, setSearchResults] = useState<SearchResponseType>(initialSearchState);
const [highlighting, setHighlighting] = useState<HighlightingResponseType>({});
const [searchItems, setSearchItems] = useState<SearchItem[]>([]);
const { ready, analytics } = useSegment();
const blockCloseEvent = useRef(false);

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

// sort result items based on matched field and its priority
const resultCategories = useMemo(
() =>
searchResults.docs.reduce<SearchCategories>(
(acc, curr) => {
if (highlighting[curr.id]?.allTitle) {
return {
...acc,
highLevel: [...acc.highLevel, curr],
};
}

if (highlighting[curr.id]?.abstract) {
return {
...acc,
midLevel: [...acc.midLevel, curr],
};
}

return {
...acc,
lowLevel: [...acc.lowLevel, curr],
};
},
{
highLevel: [],
midLevel: [],
lowLevel: [],
}
),
[searchResults.docs, highlighting]
);
const resultCount = searchItems.length;

const handleMenuKeys = (event: KeyboardEvent) => {
if (!isOpen) {
Expand All @@ -155,7 +119,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 @@ -213,28 +177,52 @@ const SearchInput = ({ onStateChange }: SearchInputListener) => {
};
}, [isOpen, menuRef]);

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 }) => {
if (isMounted.current) {
setSearchResults(response);
setHighlighting(highlighting);
// make sure to calculate resize when switching from loading to sucess state
handleWindowResize();
}
if (ready && analytics) {
analytics.track('chrome.search-query', { query: value });
}
})
.finally(() => {
isMounted.current && setIsFetching(false);
});
const handleFetch = async (value = '') => {
const response = (await fetch(SUGGEST_SEARCH_QUERY.replaceAll(REPLACE_TAG, value)).then((r) => r.json())) as SearchAutoSuggestionResponseType;

const items = (response?.suggest?.default[value]?.suggestions || []).map((suggestion) => {
const [allTitle, bundleTitle, abstract] = suggestion.term.split(AUTOSUGGEST_TERM_DELIMITER);
const url = new URL(suggestion.payload);
const pathname = url.pathname;
const item = {
title: allTitle,
bundleTitle,
description: abstract,
pathname,
};
// wrap multiple terms in quotes - otherwise search treats each as an individual term to search
return { item, allTitle };
});
const suggests = uniq(items.map(({ allTitle }) => allTitle.replace(/(<b>|<\/b>)/gm, '').trim()));
let searchItems = items.map(({ item }) => item);
console.log(suggests);
if (items.length < 10) {
console.log({ value });
const altTitleResults = (await fetch(
BASE_URL.toString()
.replaceAll(REPLACE_TAG, `(${suggests.join(' OR ')} OR ${value})`)
.replaceAll(REPLACE_COUNT_TAG, '10')
).then((r) => r.json())) as { response: SearchResponseType };
searchItems = searchItems.concat(
altTitleResults.response.docs.map((doc) => ({
pathname: doc.relative_uri,
bundleTitle: doc.bundle_title[0],
title: doc.allTitle,
description: doc.abstract,
}))
);
}
searchItems = uniqWith(searchItems, (a, b) => a.title.replace(/(<b>|<\/b>)/gm, '').trim() === b.title.replace(/(<b>|<\/b>)/gm, '').trim());
setSearchItems(searchItems.slice(0, 10));
isMounted.current && setIsFetching(false);
if (ready && analytics) {
analytics.track('chrome.search-query', { query: value });
}
};

const debouncedFetch = useCallback(debounce(handleFetch, 500), []);

const handleChange = (_e: any, value: string) => {
const handleChange: SearchInputProps['onChange'] = (_e, value) => {
setSearchValue(value);
setIsFetching(true);
debouncedFetch(value);
Expand Down Expand Up @@ -262,7 +250,7 @@ const SearchInput = ({ onStateChange }: SearchInputListener) => {
onChange={handleChange}
onClear={(ev) => {
setSearchValue('');
setSearchResults(initialSearchState);
setSearchItems([]);
ev.stopPropagation();
setIsOpen(false);
onStateChange(false);
Expand All @@ -280,7 +268,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 +278,17 @@ 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={searchItems.length > 0 ? `Top ${searchItems.length} results` : undefined}>
{searchItems.map((item, index) => (
<MenuItem key={index} className="pf-v5-u-mb-xs" component={(props) => <ChromeLink {...props} href={item.pathname} />}>
<SearchTitle title={item.title} bundleTitle={item.bundleTitle.replace(/(\[|\])/gm, '')} />
<SearchDescription description={item.description} />
</MenuItem>
))}
</MenuGroup>
</>
)}
{searchResults.numFound === 0 && !isFetching && <EmptySearchState />}
{searchItems.length === 0 && !isFetching && <EmptySearchState />}
</MenuList>
</MenuContent>
</Menu>
Expand Down
Loading

0 comments on commit e740df9

Please sign in to comment.