list.tsx 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118
  1. import {Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import AutoComplete from 'sentry/components/autoComplete';
  4. import LoadingIndicator from 'sentry/components/loadingIndicator';
  5. import {t} from 'sentry/locale';
  6. import space from 'sentry/styles/space';
  7. import {Result} from './sources/types';
  8. import SearchResult from './searchResult';
  9. import SearchResultWrapper from './searchResultWrapper';
  10. type AutoCompleteOpts = Parameters<AutoComplete<Result['item']>['props']['children']>[0];
  11. interface RenderItemProps {
  12. highlighted: boolean;
  13. item: Result['item'];
  14. itemProps: ReturnType<AutoCompleteOpts['getItemProps']>;
  15. matches: Result['matches'];
  16. }
  17. type Props = {
  18. getItemProps: AutoCompleteOpts['getItemProps'];
  19. hasAnyResults: boolean;
  20. highlightedIndex: number;
  21. isLoading: boolean;
  22. resultFooter: React.ReactNode;
  23. results: Result[];
  24. dropdownClassName?: string;
  25. maxResults?: number;
  26. renderItem?: (props: RenderItemProps) => React.ReactNode;
  27. };
  28. function defaultItemRenderer({item, highlighted, itemProps, matches}: RenderItemProps) {
  29. return (
  30. <SearchResultWrapper highlighted={highlighted} {...itemProps}>
  31. <SearchResult highlighted={highlighted} item={item} matches={matches} />
  32. </SearchResultWrapper>
  33. );
  34. }
  35. function List({
  36. dropdownClassName,
  37. isLoading,
  38. hasAnyResults,
  39. results,
  40. maxResults,
  41. getItemProps,
  42. highlightedIndex,
  43. resultFooter,
  44. renderItem = defaultItemRenderer,
  45. }: Props) {
  46. const resultList = results.slice(0, maxResults);
  47. return (
  48. <DropdownBox className={dropdownClassName}>
  49. {isLoading ? (
  50. <LoadingWrapper>
  51. <LoadingIndicator mini hideMessage relative />
  52. </LoadingWrapper>
  53. ) : !hasAnyResults ? (
  54. <EmptyItem>{t('No results found')}</EmptyItem>
  55. ) : (
  56. resultList.map((result, index) => {
  57. const {item, matches, refIndex} = result;
  58. const highlighted = index === highlightedIndex;
  59. const itemProps = getItemProps({
  60. item: result.item,
  61. index,
  62. });
  63. return (
  64. <Fragment key={`${index}-${refIndex}`}>
  65. {renderItem({highlighted, itemProps, item, matches})}
  66. </Fragment>
  67. );
  68. })
  69. )}
  70. {!isLoading && resultFooter ? <ResultFooter>{resultFooter}</ResultFooter> : null}
  71. </DropdownBox>
  72. );
  73. }
  74. export default List;
  75. const DropdownBox = styled('div')`
  76. background: ${p => p.theme.background};
  77. border: 1px solid ${p => p.theme.border};
  78. box-shadow: ${p => p.theme.dropShadowHeavy};
  79. position: absolute;
  80. top: 36px;
  81. right: 0;
  82. width: 400px;
  83. border-radius: 5px;
  84. overflow: auto;
  85. max-height: 60vh;
  86. `;
  87. const ResultFooter = styled('div')`
  88. position: sticky;
  89. bottom: 0;
  90. left: 0;
  91. right: 0;
  92. `;
  93. const EmptyItem = styled(SearchResultWrapper)`
  94. text-align: center;
  95. padding: 16px;
  96. opacity: 0.5;
  97. `;
  98. const LoadingWrapper = styled('div')`
  99. display: flex;
  100. justify-content: center;
  101. align-items: center;
  102. padding: ${space(1)};
  103. `;