Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 12 additions & 94 deletions src/components/search/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,19 @@ import {
SentryGlobalSearch,
standardSDKSlug,
} from '@sentry-internal/global-search';
import DOMPurify from 'dompurify';
import Link from 'next/link';
import {usePathname, useRouter} from 'next/navigation';
import {usePathname} from 'next/navigation';
import algoliaInsights from 'search-insights';

import {useOnClickOutside} from 'sentry-docs/clientUtils';
import {useKeyboardNavigate} from 'sentry-docs/hooks/useKeyboardNavigate';
import {isDeveloperDocs} from 'sentry-docs/isDeveloperDocs';

import styles from './search.module.scss';

import {Logo} from '../logo';

import {SearchResultItems} from './searchResultItems';
import {relativizeUrl} from './util';

// Initialize Algolia Insights
algoliaInsights('init', {
appId: process.env.NEXT_PUBLIC_ALGOLIA_APP_ID,
Expand All @@ -33,8 +33,6 @@ algoliaInsights('init', {
// treat it as a random user.
const randomUserToken = crypto.randomUUID();

const MAX_HITS = 10;

// this type is not exported from the global-search package
type SentryGlobalSearchConfig = ConstructorParameters<typeof SentryGlobalSearch>[0];

Expand All @@ -59,12 +57,6 @@ const userDocsSites: SentryGlobalSearchConfig = [
const config = isDeveloperDocs ? developerDocsSites : userDocsSites;
const search = new SentryGlobalSearch(config);

function relativizeUrl(url: string) {
return isDeveloperDocs
? url
: url.replace(/^(https?:\/\/docs\.sentry\.io)(?=\/|$)/, '');
}

type Props = {
autoFocus?: boolean;
path?: string;
Expand All @@ -79,7 +71,7 @@ export function Search({path, autoFocus, searchPlatforms = [], showChatBot}: Pro
const [inputFocus, setInputFocus] = useState(false);
const [showOffsiteResults, setShowOffsiteResults] = useState(false);
const [loading, setLoading] = useState(true);
const router = useRouter();

const pathname = usePathname();

const handleClickOutside = useCallback((ev: MouseEvent) => {
Expand Down Expand Up @@ -176,16 +168,6 @@ export function Search({path, autoFocus, searchPlatforms = [], showChatBot}: Pro

const totalHits = results.reduce((a, x) => a + x.hits.length, 0);

const flatHits = results.reduce<Hit[]>(
(items, item) => [...items, ...item.hits.slice(0, MAX_HITS)],
[]
);

const {focused} = useKeyboardNavigate({
list: flatHits,
onSelect: hit => router.push(relativizeUrl(hit.url)),
});

const trackSearchResultClick = useCallback((hit: Hit, position: number): void => {
try {
algoliaInsights('clickedObjectIDsAfterSearch', {
Expand Down Expand Up @@ -305,77 +287,13 @@ export function Search({path, autoFocus, searchPlatforms = [], showChatBot}: Pro
{loading && <Logo loading />}

{!loading && totalHits > 0 && (
<div className={styles['sgs-search-results-scroll-container']}>
{results
.filter(x => x.hits.length > 0)
.map((result, i) => (
<Fragment key={result.site}>
{showOffsiteResults && (
<h4 className={styles['sgs-site-result-heading']}>
From {result.name}
</h4>
)}
<ul
className={`${styles['sgs-hit-list']} ${i === 0 ? '' : styles['sgs-offsite']}`}
>
{result.hits.slice(0, MAX_HITS).map((hit, index) => (
<li
key={hit.id}
className={`${styles['sgs-hit-item']} ${
focused?.id === hit.id ? styles['sgs-hit-focused'] : ''
}`}
ref={
// Scroll to element on focus
hit.id === focused?.id
? el => el?.scrollIntoView({block: 'nearest'})
: undefined
}
>
<Link
href={relativizeUrl(hit.url)}
onClick={e => handleSearchResultClick(e, hit, index)}
>
{hit.title && (
<h6>
<span
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(hit.title, {
ALLOWED_TAGS: ['mark'],
}),
}}
/>
</h6>
)}
{hit.text && (
<span
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(hit.text, {
ALLOWED_TAGS: ['mark'],
}),
}}
/>
)}
{hit.context && (
<div className={styles['sgs-hit-context']}>
{hit.context.context1 && (
<div className={styles['sgs-hit-context-left']}>
{hit.context.context1}
</div>
)}
{hit.context.context2 && (
<div className={styles['sgs-hit-context-right']}>
{hit.context.context2}
</div>
)}
</div>
)}
</Link>
</li>
))}
</ul>
</Fragment>
))}
</div>
<SearchResultItems
results={results}
onSearchResultClick={({event, hit, position}) =>
handleSearchResultClick(event, hit, position)
}
showOffsiteResults={showOffsiteResults}
/>
)}

{!loading && totalHits === 0 && (
Expand Down
111 changes: 111 additions & 0 deletions src/components/search/searchResultItems.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import {Fragment} from 'react';
import {Hit, Result} from '@sentry-internal/global-search';
import DOMPurify from 'dompurify';
import Link from 'next/link';
import {useRouter} from 'next/navigation';

import {useListKeyboardNavigate} from 'sentry-docs/hooks/useListKeyboardNavigate';

import styles from './search.module.scss';

import {relativizeUrl} from './util';

const MAX_HITS = 10;

interface SearchResultClickHandler {
event: React.MouseEvent<HTMLAnchorElement>;
hit: Hit;
position: number;
}

export function SearchResultItems({
results,
showOffsiteResults,
onSearchResultClick,
}: {
onSearchResultClick: (params: SearchResultClickHandler) => void;
results: Result[];
showOffsiteResults: boolean;
}) {
const router = useRouter();
const flatHits = results.reduce<Hit[]>(
(items, item) => [...items, ...item.hits.slice(0, MAX_HITS)],
[]
);
const {focused} = useListKeyboardNavigate({
list: flatHits,
onSelect: hit => router.push(relativizeUrl(hit.url)),
});

return (
<div className={styles['sgs-search-results-scroll-container']}>
{results
.filter(x => x.hits.length > 0)
.map((result, i) => (
<Fragment key={result.site}>
{showOffsiteResults && (
<h4 className={styles['sgs-site-result-heading']}>From {result.name}</h4>
)}
<ul
className={`${styles['sgs-hit-list']} ${i === 0 ? '' : styles['sgs-offsite']}`}
>
{result.hits.slice(0, MAX_HITS).map((hit, index) => (
<li
key={hit.id}
className={`${styles['sgs-hit-item']} ${
focused?.id === hit.id ? styles['sgs-hit-focused'] : ''
}`}
ref={
// Scroll to element on focus
hit.id === focused?.id
? el => el?.scrollIntoView({block: 'nearest'})
: undefined
}
>
<Link
href={relativizeUrl(hit.url)}
onClick={event => onSearchResultClick({event, hit, position: index})}
>
{hit.title && (
<h6>
<span
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(hit.title, {
ALLOWED_TAGS: ['mark'],
}),
}}
/>
</h6>
)}
{hit.text && (
<span
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(hit.text, {
ALLOWED_TAGS: ['mark'],
}),
}}
/>
)}
{hit.context && (
<div className={styles['sgs-hit-context']}>
{hit.context.context1 && (
<div className={styles['sgs-hit-context-left']}>
{hit.context.context1}
</div>
)}
{hit.context.context2 && (
<div className={styles['sgs-hit-context-right']}>
{hit.context.context2}
</div>
)}
</div>
)}
</Link>
</li>
))}
</ul>
</Fragment>
))}
</div>
);
}
7 changes: 7 additions & 0 deletions src/components/search/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import {isDeveloperDocs} from 'sentry-docs/isDeveloperDocs';

export function relativizeUrl(url: string) {
return isDeveloperDocs
? url
: url.replace(/^(https?:\/\/docs\.sentry\.io)(?=\/|$)/, '');
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ type Props<T> = {
* The list of values to navigate through
*/
list: T[];

/**
* Callback triggered when the item is selected
*/
Expand All @@ -14,7 +15,7 @@ type Props<T> = {
/**
* Navigate a list of items using the up/down arrow and ^j/^k keys
*/
function useKeyboardNavigate<T>({list, onSelect}: Props<T>) {
function useListKeyboardNavigate<T>({list, onSelect}: Props<T>) {
const [focused, setFocus] = useState<T | null>(null);

const setFocusIndex = useCallback(
Expand Down Expand Up @@ -92,4 +93,4 @@ function useKeyboardNavigate<T>({list, onSelect}: Props<T>) {
return {focused, setFocus};
}

export {useKeyboardNavigate};
export {useListKeyboardNavigate};
Loading