helpSource.tsx 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  1. import {Component} from 'react';
  2. // eslint-disable-next-line no-restricted-imports
  3. import {withRouter, WithRouterProps} from 'react-router';
  4. import {
  5. Result as SearchResult,
  6. SentryGlobalSearch,
  7. standardSDKSlug,
  8. } from '@sentry-internal/global-search';
  9. import dompurify from 'dompurify';
  10. import debounce from 'lodash/debounce';
  11. import {Organization, Project} from 'sentry/types';
  12. import parseHtmlMarks from 'sentry/utils/parseHtmlMarks';
  13. import withLatestContext from 'sentry/utils/withLatestContext';
  14. import {ChildProps, Result, ResultItem} from './types';
  15. type Props = WithRouterProps & {
  16. /**
  17. * Render function that renders the global search result
  18. */
  19. children: (props: ChildProps) => React.ReactNode;
  20. organization: Organization;
  21. /**
  22. * Specific platforms to filter results to
  23. */
  24. platforms: string[];
  25. project: Project;
  26. /**
  27. * The string to search the navigation routes for
  28. */
  29. query: string;
  30. };
  31. type State = {
  32. loading: boolean;
  33. results: Result[];
  34. };
  35. const MARK_TAGS = {
  36. highlightPreTag: '<mark>',
  37. highlightPostTag: '</mark>',
  38. };
  39. class HelpSource extends Component<Props, State> {
  40. state: State = {
  41. loading: false,
  42. results: [],
  43. };
  44. componentDidMount() {
  45. if (this.props.query !== undefined) {
  46. this.doSearch(this.props.query);
  47. }
  48. }
  49. componentDidUpdate(nextProps: Props) {
  50. if (nextProps.query !== this.props.query) {
  51. this.doSearch(nextProps.query);
  52. }
  53. }
  54. search = new SentryGlobalSearch(['docs', 'help-center', 'develop', 'blog']);
  55. async unbouncedSearch(query: string) {
  56. this.setState({loading: true});
  57. const {platforms = []} = this.props;
  58. const searchResults = await this.search.query(query, {
  59. platforms: platforms.map(platform => standardSDKSlug(platform)?.slug!),
  60. });
  61. const results = mapSearchResults(searchResults);
  62. this.setState({loading: false, results});
  63. }
  64. doSearch = debounce(this.unbouncedSearch, 300);
  65. render() {
  66. return this.props.children({
  67. isLoading: this.state.loading,
  68. results: this.state.results,
  69. });
  70. }
  71. }
  72. function mapSearchResults(results: SearchResult[]) {
  73. const items: Result[] = [];
  74. results.forEach(section => {
  75. const sectionItems = section.hits.map(hit => {
  76. const title = parseHtmlMarks({
  77. key: 'title',
  78. htmlString: hit.title ?? '',
  79. markTags: MARK_TAGS,
  80. });
  81. const description = parseHtmlMarks({
  82. key: 'description',
  83. htmlString: hit.text ?? '',
  84. markTags: MARK_TAGS,
  85. });
  86. const item: ResultItem = {
  87. ...hit,
  88. sourceType: 'help',
  89. resultType: `help-${hit.site}` as ResultItem['resultType'],
  90. title: dompurify.sanitize(hit.title ?? ''),
  91. extra: hit.context.context1,
  92. description: hit.text ? dompurify.sanitize(hit.text) : undefined,
  93. to: hit.url,
  94. };
  95. return {item, matches: [title, description], score: 1, refIndex: 0};
  96. });
  97. // The first element should indicate the section.
  98. if (sectionItems.length > 0) {
  99. sectionItems[0].item.sectionHeading = section.name;
  100. sectionItems[0].item.sectionCount = sectionItems.length;
  101. items.push(...sectionItems);
  102. return;
  103. }
  104. // If we didn't have any results for this section mark it as empty
  105. const emptyHeaderItem: ResultItem = {
  106. sourceType: 'help',
  107. resultType: `help-${section.site}` as ResultItem['resultType'],
  108. title: `No results in ${section.name}`,
  109. sectionHeading: section.name,
  110. empty: true,
  111. };
  112. items.push({item: emptyHeaderItem, score: 1, refIndex: 0});
  113. });
  114. return items;
  115. }
  116. export {HelpSource};
  117. export default withLatestContext(withRouter(HelpSource));