SearchResults.tsx 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. import { css, cx } from '@emotion/css';
  2. import React, { FC, memo } from 'react';
  3. import AutoSizer from 'react-virtualized-auto-sizer';
  4. import { FixedSizeList, FixedSizeGrid } from 'react-window';
  5. import { GrafanaTheme } from '@grafana/data';
  6. import { selectors } from '@grafana/e2e-selectors';
  7. import { Spinner, stylesFactory, useTheme } from '@grafana/ui';
  8. import { SEARCH_ITEM_HEIGHT, SEARCH_ITEM_MARGIN } from '../constants';
  9. import { DashboardSection, OnToggleChecked, SearchLayout } from '../types';
  10. import { SearchCard } from './SearchCard';
  11. import { SearchItem } from './SearchItem';
  12. import { SectionHeader } from './SectionHeader';
  13. export interface Props {
  14. editable?: boolean;
  15. loading?: boolean;
  16. onTagSelected: (name: string) => any;
  17. onToggleChecked?: OnToggleChecked;
  18. onToggleSection: (section: DashboardSection) => void;
  19. results: DashboardSection[];
  20. showPreviews?: boolean;
  21. layout?: string;
  22. }
  23. const { sectionV2: sectionLabel, itemsV2: itemsLabel, cards: cardsLabel } = selectors.components.Search;
  24. export const SearchResults: FC<Props> = memo(
  25. ({ editable, loading, onTagSelected, onToggleChecked, onToggleSection, results, showPreviews, layout }) => {
  26. const theme = useTheme();
  27. const styles = getSectionStyles(theme);
  28. const itemProps = { editable, onToggleChecked, onTagSelected };
  29. const renderFolders = () => {
  30. const Wrapper = showPreviews ? SearchCard : SearchItem;
  31. return (
  32. <div className={styles.wrapper}>
  33. {results.map((section) => {
  34. return (
  35. <div data-testid={sectionLabel} className={styles.section} key={section.id || section.title}>
  36. {section.title && (
  37. <SectionHeader onSectionClick={onToggleSection} {...{ onToggleChecked, editable, section }}>
  38. <div
  39. data-testid={showPreviews ? cardsLabel : itemsLabel}
  40. className={cx(styles.sectionItems, { [styles.gridContainer]: showPreviews })}
  41. >
  42. {section.items.map((item) => (
  43. <Wrapper {...itemProps} key={item.uid} item={item} />
  44. ))}
  45. </div>
  46. </SectionHeader>
  47. )}
  48. </div>
  49. );
  50. })}
  51. </div>
  52. );
  53. };
  54. const renderDashboards = () => {
  55. const items = results[0]?.items;
  56. return (
  57. <div className={styles.listModeWrapper}>
  58. <AutoSizer>
  59. {({ height, width }) => {
  60. const numColumns = Math.ceil(width / 320);
  61. const cellWidth = width / numColumns;
  62. const cellHeight = (cellWidth - 64) * 0.75 + 56 + 8;
  63. const numRows = Math.ceil(items.length / numColumns);
  64. return showPreviews ? (
  65. <FixedSizeGrid
  66. columnCount={numColumns}
  67. columnWidth={cellWidth}
  68. rowCount={numRows}
  69. rowHeight={cellHeight}
  70. className={styles.wrapper}
  71. innerElementType="ul"
  72. height={height}
  73. width={width}
  74. >
  75. {({ columnIndex, rowIndex, style }) => {
  76. const index = rowIndex * numColumns + columnIndex;
  77. const item = items[index];
  78. // The wrapper div is needed as the inner SearchItem has margin-bottom spacing
  79. // And without this wrapper there is no room for that margin
  80. return item ? (
  81. <li style={style} className={styles.virtualizedGridItemWrapper}>
  82. <SearchCard key={item.id} {...itemProps} item={item} />
  83. </li>
  84. ) : null;
  85. }}
  86. </FixedSizeGrid>
  87. ) : (
  88. <FixedSizeList
  89. className={styles.wrapper}
  90. innerElementType="ul"
  91. itemSize={SEARCH_ITEM_HEIGHT + SEARCH_ITEM_MARGIN}
  92. height={height}
  93. itemCount={items.length}
  94. width={width}
  95. >
  96. {({ index, style }) => {
  97. const item = items[index];
  98. // The wrapper div is needed as the inner SearchItem has margin-bottom spacing
  99. // And without this wrapper there is no room for that margin
  100. return (
  101. <li style={style}>
  102. <SearchItem key={item.id} {...itemProps} item={item} />
  103. </li>
  104. );
  105. }}
  106. </FixedSizeList>
  107. );
  108. }}
  109. </AutoSizer>
  110. </div>
  111. );
  112. };
  113. if (loading) {
  114. return <Spinner className={styles.spinner} />;
  115. } else if (!results || !results.length) {
  116. return <div className={styles.noResults}>No dashboards matching your query were found.</div>;
  117. }
  118. return (
  119. <div className={styles.resultsContainer}>
  120. {layout === SearchLayout.Folders ? renderFolders() : renderDashboards()}
  121. </div>
  122. );
  123. }
  124. );
  125. SearchResults.displayName = 'SearchResults';
  126. const getSectionStyles = stylesFactory((theme: GrafanaTheme) => {
  127. const { md, sm } = theme.spacing;
  128. return {
  129. virtualizedGridItemWrapper: css`
  130. padding: 4px;
  131. `,
  132. wrapper: css`
  133. display: flex;
  134. flex-direction: column;
  135. > ul {
  136. list-style: none;
  137. }
  138. `,
  139. section: css`
  140. display: flex;
  141. flex-direction: column;
  142. background: ${theme.colors.panelBg};
  143. border-bottom: solid 1px ${theme.colors.border2};
  144. `,
  145. sectionItems: css`
  146. margin: 0 24px 0 32px;
  147. `,
  148. spinner: css`
  149. display: flex;
  150. justify-content: center;
  151. align-items: center;
  152. min-height: 100px;
  153. `,
  154. gridContainer: css`
  155. display: grid;
  156. gap: ${sm};
  157. grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  158. margin-bottom: ${md};
  159. `,
  160. resultsContainer: css`
  161. position: relative;
  162. flex-grow: 10;
  163. margin-bottom: ${md};
  164. background: ${theme.colors.bg1};
  165. border: 1px solid ${theme.colors.border1};
  166. border-radius: 3px;
  167. height: 100%;
  168. `,
  169. noResults: css`
  170. padding: ${md};
  171. background: ${theme.colors.bg2};
  172. font-style: italic;
  173. margin-top: ${theme.spacing.md};
  174. `,
  175. listModeWrapper: css`
  176. position: relative;
  177. height: 100%;
  178. padding: ${md};
  179. `,
  180. };
  181. });