FolderSection.tsx 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  1. import { css, cx } from '@emotion/css';
  2. import React, { FC } from 'react';
  3. import { useAsync, useLocalStorage } from 'react-use';
  4. import { GrafanaTheme } from '@grafana/data';
  5. import { Card, Checkbox, CollapsableSection, Icon, Spinner, stylesFactory, useTheme } from '@grafana/ui';
  6. import { getSectionStorageKey } from 'app/features/search/utils';
  7. import { useUniqueId } from 'app/plugins/datasource/influxdb/components/useUniqueId';
  8. import { SearchItem } from '../..';
  9. import { getGrafanaSearcher, SearchQuery } from '../../service';
  10. import { DashboardSearchItemType, DashboardSectionItem } from '../../types';
  11. import { SelectionChecker, SelectionToggle } from '../selection';
  12. export interface DashboardSection {
  13. kind: string; // folder | query!
  14. uid: string;
  15. title: string;
  16. selected?: boolean; // not used ? keyboard
  17. url?: string;
  18. icon?: string;
  19. itemsUIDs?: string[]; // for pseudo folders
  20. }
  21. interface SectionHeaderProps {
  22. selection?: SelectionChecker;
  23. selectionToggle?: SelectionToggle;
  24. onTagSelected: (tag: string) => void;
  25. section: DashboardSection;
  26. renderStandaloneBody?: boolean; // render the body on its own
  27. tags?: string[];
  28. }
  29. export const FolderSection: FC<SectionHeaderProps> = ({
  30. section,
  31. selectionToggle,
  32. onTagSelected,
  33. selection,
  34. renderStandaloneBody,
  35. tags,
  36. }) => {
  37. const editable = selectionToggle != null;
  38. const theme = useTheme();
  39. const styles = getSectionHeaderStyles(theme, section.selected, editable);
  40. const [sectionExpanded, setSectionExpanded] = useLocalStorage(getSectionStorageKey(section.title), false);
  41. const results = useAsync(async () => {
  42. if (!sectionExpanded && !renderStandaloneBody) {
  43. return Promise.resolve([] as DashboardSectionItem[]);
  44. }
  45. let folderUid: string | undefined = section.uid;
  46. let folderTitle: string | undefined = section.title;
  47. let query: SearchQuery = {
  48. query: '*',
  49. kind: ['dashboard'],
  50. location: section.uid,
  51. sort: 'name_sort',
  52. };
  53. if (section.itemsUIDs) {
  54. query = {
  55. uid: section.itemsUIDs, // array of UIDs
  56. };
  57. folderUid = undefined;
  58. folderTitle = undefined;
  59. }
  60. const raw = await getGrafanaSearcher().search({ ...query, tags });
  61. const v = raw.view.map(
  62. (item) =>
  63. ({
  64. uid: item.uid,
  65. title: item.name,
  66. url: item.url,
  67. uri: item.url,
  68. type: item.kind === 'folder' ? DashboardSearchItemType.DashFolder : DashboardSearchItemType.DashDB,
  69. id: 666, // do not use me!
  70. isStarred: false,
  71. tags: item.tags ?? [],
  72. folderUid,
  73. folderTitle,
  74. } as DashboardSectionItem)
  75. );
  76. return v;
  77. }, [sectionExpanded, section, tags]);
  78. const onSectionExpand = () => {
  79. setSectionExpanded(!sectionExpanded);
  80. };
  81. const onToggleFolder = (evt: React.FormEvent) => {
  82. evt.preventDefault();
  83. evt.stopPropagation();
  84. if (selectionToggle && selection) {
  85. const checked = !selection(section.kind, section.uid);
  86. selectionToggle(section.kind, section.uid);
  87. const sub = results.value ?? [];
  88. for (const item of sub) {
  89. if (selection('dashboard', item.uid!) !== checked) {
  90. selectionToggle('dashboard', item.uid!);
  91. }
  92. }
  93. }
  94. };
  95. const onToggleChecked = (item: DashboardSectionItem) => {
  96. if (selectionToggle) {
  97. selectionToggle('dashboard', item.uid!);
  98. }
  99. };
  100. const id = useUniqueId();
  101. const labelId = `section-header-label-${id}`;
  102. let icon = section.icon;
  103. if (!icon) {
  104. icon = sectionExpanded ? 'folder-open' : 'folder';
  105. }
  106. const renderResults = () => {
  107. if (!results.value?.length) {
  108. if (results.loading) {
  109. return <Spinner />;
  110. }
  111. return (
  112. <Card>
  113. <Card.Heading>No results found</Card.Heading>
  114. </Card>
  115. );
  116. }
  117. return results.value.map((v) => {
  118. if (selection && selectionToggle) {
  119. const type = v.type === DashboardSearchItemType.DashFolder ? 'folder' : 'dashboard';
  120. v = {
  121. ...v,
  122. checked: selection(type, v.uid!),
  123. };
  124. }
  125. return (
  126. <SearchItem
  127. key={v.uid}
  128. item={v}
  129. onTagSelected={onTagSelected}
  130. onToggleChecked={onToggleChecked as any}
  131. editable={Boolean(selection != null)}
  132. />
  133. );
  134. });
  135. };
  136. // Skip the folder wrapper
  137. if (renderStandaloneBody) {
  138. return <div className={styles.folderViewResults}>{renderResults()}</div>;
  139. }
  140. return (
  141. <CollapsableSection
  142. isOpen={sectionExpanded ?? false}
  143. onToggle={onSectionExpand}
  144. className={styles.wrapper}
  145. contentClassName={styles.content}
  146. loading={results.loading}
  147. labelId={labelId}
  148. label={
  149. <>
  150. {selectionToggle && selection && (
  151. <div className={styles.checkbox} onClick={onToggleFolder}>
  152. <Checkbox value={selection(section.kind, section.uid)} aria-label="Select folder" />
  153. </div>
  154. )}
  155. <div className={styles.icon}>
  156. <Icon name={icon as any} />
  157. </div>
  158. <div className={styles.text}>
  159. <span id={labelId}>{section.title}</span>
  160. {section.url && section.uid !== 'general' && (
  161. <a href={section.url} className={styles.link}>
  162. <span className={styles.separator}>|</span> <Icon name="folder-upload" /> Go to folder
  163. </a>
  164. )}
  165. </div>
  166. </>
  167. }
  168. >
  169. {results.value && <ul className={styles.sectionItems}>{renderResults()}</ul>}
  170. </CollapsableSection>
  171. );
  172. };
  173. const getSectionHeaderStyles = stylesFactory((theme: GrafanaTheme, selected = false, editable: boolean) => {
  174. const { sm } = theme.spacing;
  175. return {
  176. wrapper: cx(
  177. css`
  178. align-items: center;
  179. font-size: ${theme.typography.size.base};
  180. padding: 12px;
  181. border-bottom: none;
  182. color: ${theme.colors.textWeak};
  183. z-index: 1;
  184. &:hover,
  185. &.selected {
  186. color: ${theme.colors.text};
  187. }
  188. &:hover,
  189. &:focus-visible,
  190. &:focus-within {
  191. a {
  192. opacity: 1;
  193. }
  194. }
  195. `,
  196. 'pointer',
  197. { selected }
  198. ),
  199. sectionItems: css`
  200. margin: 0 24px 0 32px;
  201. `,
  202. checkbox: css`
  203. padding: 0 ${sm} 0 0;
  204. `,
  205. icon: css`
  206. padding: 0 ${sm} 0 ${editable ? 0 : sm};
  207. `,
  208. folderViewResults: css`
  209. overflow: auto;
  210. `,
  211. text: css`
  212. flex-grow: 1;
  213. line-height: 24px;
  214. `,
  215. link: css`
  216. padding: 2px 10px 0;
  217. color: ${theme.colors.textWeak};
  218. opacity: 0;
  219. transition: opacity 150ms ease-in-out;
  220. `,
  221. separator: css`
  222. margin-right: 6px;
  223. `,
  224. content: css`
  225. padding-top: 0px;
  226. padding-bottom: 0px;
  227. `,
  228. };
  229. });