SearchResultsGrid.tsx 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. import { css } from '@emotion/css';
  2. import React from 'react';
  3. import { FixedSizeGrid } from 'react-window';
  4. import InfiniteLoader from 'react-window-infinite-loader';
  5. import { GrafanaTheme2 } from '@grafana/data';
  6. import { config } from '@grafana/runtime';
  7. import { useStyles2 } from '@grafana/ui';
  8. import { SearchCard } from '../../components/SearchCard';
  9. import { useSearchKeyboardNavigation } from '../../hooks/useSearchKeyboardSelection';
  10. import { DashboardSearchItemType, DashboardSectionItem } from '../../types';
  11. import { SearchResultsProps } from './SearchResultsTable';
  12. export const SearchResultsGrid = ({
  13. response,
  14. width,
  15. height,
  16. selection,
  17. selectionToggle,
  18. onTagSelected,
  19. keyboardEvents,
  20. }: SearchResultsProps) => {
  21. const styles = useStyles2(getStyles);
  22. // Hacked to reuse existing SearchCard (and old DashboardSectionItem)
  23. const itemProps = {
  24. editable: selection != null,
  25. onToggleChecked: (item: any) => {
  26. const d = item as DashboardSectionItem;
  27. const t = d.type === DashboardSearchItemType.DashFolder ? 'folder' : 'dashboard';
  28. if (selectionToggle) {
  29. selectionToggle(t, d.uid!);
  30. }
  31. },
  32. onTagSelected,
  33. };
  34. const itemCount = response.totalRows ?? response.view.length;
  35. const view = response.view;
  36. const numColumns = Math.ceil(width / 320);
  37. const cellWidth = width / numColumns;
  38. const cellHeight = (cellWidth - 64) * 0.75 + 56 + 8;
  39. const numRows = Math.ceil(itemCount / numColumns);
  40. const highlightIndex = useSearchKeyboardNavigation(keyboardEvents, numColumns, response);
  41. return (
  42. <InfiniteLoader isItemLoaded={response.isItemLoaded} itemCount={itemCount} loadMoreItems={response.loadMoreItems}>
  43. {({ onItemsRendered, ref }) => (
  44. <FixedSizeGrid
  45. ref={ref}
  46. onItemsRendered={(v) => {
  47. onItemsRendered({
  48. visibleStartIndex: v.visibleRowStartIndex * numColumns,
  49. visibleStopIndex: v.visibleRowStopIndex * numColumns,
  50. overscanStartIndex: v.overscanRowStartIndex * numColumns,
  51. overscanStopIndex: v.overscanColumnStopIndex * numColumns,
  52. });
  53. }}
  54. columnCount={numColumns}
  55. columnWidth={cellWidth}
  56. rowCount={numRows}
  57. rowHeight={cellHeight}
  58. className={styles.wrapper}
  59. innerElementType="ul"
  60. height={height}
  61. width={width - 2}
  62. >
  63. {({ columnIndex, rowIndex, style }) => {
  64. const index = rowIndex * numColumns + columnIndex;
  65. if (index >= view.length) {
  66. return null;
  67. }
  68. const item = view.get(index);
  69. const kind = item.kind ?? 'dashboard';
  70. const facade: DashboardSectionItem = {
  71. uid: item.uid,
  72. title: item.name,
  73. url: item.url,
  74. uri: item.url,
  75. type: kind === 'folder' ? DashboardSearchItemType.DashFolder : DashboardSearchItemType.DashDB,
  76. id: 666, // do not use me!
  77. isStarred: false,
  78. tags: item.tags ?? [],
  79. checked: selection ? selection(kind, item.uid) : false,
  80. };
  81. if (kind === 'panel') {
  82. const type = item.panel_type;
  83. facade.icon = 'public/img/icons/unicons/graph-bar.svg';
  84. if (type) {
  85. const info = config.panels[type];
  86. if (info?.name) {
  87. const v = info.info?.logos.small;
  88. if (v && v.endsWith('.svg')) {
  89. facade.icon = v;
  90. }
  91. }
  92. }
  93. }
  94. let className = styles.virtualizedGridItemWrapper;
  95. if (rowIndex === highlightIndex.y && columnIndex === highlightIndex.x) {
  96. className += ' ' + styles.selectedItem;
  97. }
  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 item ? (
  101. <li style={style} className={className}>
  102. <SearchCard key={item.uid} {...itemProps} item={facade} />
  103. </li>
  104. ) : null;
  105. }}
  106. </FixedSizeGrid>
  107. )}
  108. </InfiniteLoader>
  109. );
  110. };
  111. const getStyles = (theme: GrafanaTheme2) => ({
  112. virtualizedGridItemWrapper: css`
  113. padding: 4px;
  114. `,
  115. wrapper: css`
  116. display: flex;
  117. flex-direction: column;
  118. > ul {
  119. list-style: none;
  120. }
  121. `,
  122. selectedItem: css`
  123. box-shadow: inset 1px 1px 3px 3px ${theme.colors.primary.border};
  124. `,
  125. });