OptionsPaneOptions.tsx 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. import { css } from '@emotion/css';
  2. import React, { useMemo, useState } from 'react';
  3. import { GrafanaTheme2, SelectableValue } from '@grafana/data';
  4. import { CustomScrollbar, FilterInput, RadioButtonGroup, useStyles2 } from '@grafana/ui';
  5. import { isPanelModelLibraryPanel } from '../../../library-panels/guard';
  6. import { AngularPanelOptions } from './AngularPanelOptions';
  7. import { OptionsPaneCategory } from './OptionsPaneCategory';
  8. import { OptionsPaneCategoryDescriptor } from './OptionsPaneCategoryDescriptor';
  9. import { getFieldOverrideCategories } from './getFieldOverrideElements';
  10. import { getLibraryPanelOptionsCategory } from './getLibraryPanelOptions';
  11. import { getPanelFrameCategory } from './getPanelFrameOptions';
  12. import { getVisualizationOptions } from './getVisualizationOptions';
  13. import { OptionSearchEngine } from './state/OptionSearchEngine';
  14. import { getRecentOptions } from './state/getRecentOptions';
  15. import { OptionPaneRenderProps } from './types';
  16. export const OptionsPaneOptions: React.FC<OptionPaneRenderProps> = (props) => {
  17. const { plugin, dashboard, panel } = props;
  18. const [searchQuery, setSearchQuery] = useState('');
  19. const [listMode, setListMode] = useState(OptionFilter.All);
  20. const styles = useStyles2(getStyles);
  21. const [panelFrameOptions, vizOptions, libraryPanelOptions] = useMemo(
  22. () => [getPanelFrameCategory(props), getVisualizationOptions(props), getLibraryPanelOptionsCategory(props)],
  23. // eslint-disable-next-line react-hooks/exhaustive-deps
  24. [panel.configRev, props.data, props.instanceState, searchQuery]
  25. );
  26. const justOverrides = useMemo(
  27. () => getFieldOverrideCategories(props, searchQuery),
  28. // eslint-disable-next-line react-hooks/exhaustive-deps
  29. [panel.configRev, props.data, props.instanceState, searchQuery]
  30. );
  31. const mainBoxElements: React.ReactNode[] = [];
  32. const isSearching = searchQuery.length > 0;
  33. const optionRadioFilters = useMemo(getOptionRadioFilters, []);
  34. const allOptions = isPanelModelLibraryPanel(panel)
  35. ? [libraryPanelOptions, panelFrameOptions, ...vizOptions]
  36. : [panelFrameOptions, ...vizOptions];
  37. if (isSearching) {
  38. mainBoxElements.push(renderSearchHits(allOptions, justOverrides, searchQuery));
  39. // If searching for angular panel, then we need to add notice that results are limited
  40. if (props.plugin.angularPanelCtrl) {
  41. mainBoxElements.push(
  42. <div className={styles.searchNotice} key="Search notice">
  43. This is an old visualization type that does not support searching all options.
  44. </div>
  45. );
  46. }
  47. } else {
  48. switch (listMode) {
  49. case OptionFilter.All:
  50. if (isPanelModelLibraryPanel(panel)) {
  51. // Library Panel options first
  52. mainBoxElements.push(libraryPanelOptions.render());
  53. }
  54. // Panel frame options second
  55. mainBoxElements.push(panelFrameOptions.render());
  56. // If angular add those options next
  57. if (props.plugin.angularPanelCtrl) {
  58. mainBoxElements.push(
  59. <AngularPanelOptions plugin={plugin} dashboard={dashboard} panel={panel} key="AngularOptions" />
  60. );
  61. }
  62. // Then add all panel and field defaults
  63. for (const item of vizOptions) {
  64. mainBoxElements.push(item.render());
  65. }
  66. for (const item of justOverrides) {
  67. mainBoxElements.push(item.render());
  68. }
  69. break;
  70. case OptionFilter.Overrides:
  71. for (const override of justOverrides) {
  72. mainBoxElements.push(override.render());
  73. }
  74. break;
  75. case OptionFilter.Recent:
  76. mainBoxElements.push(
  77. <OptionsPaneCategory id="Recent options" title="Recent options" key="Recent options" forceOpen={1}>
  78. {getRecentOptions(allOptions).map((item) => item.render())}
  79. </OptionsPaneCategory>
  80. );
  81. break;
  82. }
  83. }
  84. // only show radio buttons if we are searching or if the plugin has field config
  85. const showSearchRadioButtons = !isSearching && !plugin.fieldConfigRegistry.isEmpty();
  86. return (
  87. <div className={styles.wrapper}>
  88. <div className={styles.formBox}>
  89. <div className={styles.formRow}>
  90. <FilterInput width={0} value={searchQuery} onChange={setSearchQuery} placeholder={'Search options'} />
  91. </div>
  92. {showSearchRadioButtons && (
  93. <div className={styles.formRow}>
  94. <RadioButtonGroup options={optionRadioFilters} value={listMode} fullWidth onChange={setListMode} />
  95. </div>
  96. )}
  97. </div>
  98. <div className={styles.scrollWrapper}>
  99. <CustomScrollbar autoHeightMin="100%">
  100. <div className={styles.mainBox}>{mainBoxElements}</div>
  101. </CustomScrollbar>
  102. </div>
  103. </div>
  104. );
  105. };
  106. function getOptionRadioFilters(): Array<SelectableValue<OptionFilter>> {
  107. return [
  108. { label: OptionFilter.All, value: OptionFilter.All },
  109. { label: OptionFilter.Overrides, value: OptionFilter.Overrides },
  110. ];
  111. }
  112. export enum OptionFilter {
  113. All = 'All',
  114. Overrides = 'Overrides',
  115. Recent = 'Recent',
  116. }
  117. function renderSearchHits(
  118. allOptions: OptionsPaneCategoryDescriptor[],
  119. overrides: OptionsPaneCategoryDescriptor[],
  120. searchQuery: string
  121. ) {
  122. const engine = new OptionSearchEngine(allOptions, overrides);
  123. const { optionHits, totalCount, overrideHits } = engine.search(searchQuery);
  124. return (
  125. <div key="search results">
  126. <OptionsPaneCategory
  127. id="Found options"
  128. title={`Matched ${optionHits.length}/${totalCount} options`}
  129. key="Normal options"
  130. forceOpen={1}
  131. >
  132. {optionHits.map((hit) => hit.render(searchQuery))}
  133. </OptionsPaneCategory>
  134. {overrideHits.map((override) => override.render(searchQuery))}
  135. </div>
  136. );
  137. }
  138. const getStyles = (theme: GrafanaTheme2) => ({
  139. wrapper: css`
  140. height: 100%;
  141. display: flex;
  142. flex-direction: column;
  143. flex: 1 1 0;
  144. .search-fragment-highlight {
  145. color: ${theme.colors.warning.text};
  146. background: transparent;
  147. }
  148. `,
  149. searchBox: css`
  150. display: flex;
  151. flex-direction: column;
  152. min-height: 0;
  153. `,
  154. formRow: css`
  155. margin-bottom: ${theme.spacing(1)};
  156. `,
  157. formBox: css`
  158. padding: ${theme.spacing(1)};
  159. background: ${theme.colors.background.primary};
  160. border: 1px solid ${theme.components.panel.borderColor};
  161. border-top-left-radius: ${theme.shape.borderRadius(1.5)};
  162. border-bottom: none;
  163. `,
  164. closeButton: css`
  165. margin-left: ${theme.spacing(1)};
  166. `,
  167. searchHits: css`
  168. padding: ${theme.spacing(1, 1, 0, 1)};
  169. `,
  170. scrollWrapper: css`
  171. flex-grow: 1;
  172. min-height: 0;
  173. `,
  174. searchNotice: css`
  175. font-size: ${theme.typography.size.sm};
  176. color: ${theme.colors.text.secondary};
  177. padding: ${theme.spacing(1)};
  178. text-align: center;
  179. `,
  180. mainBox: css`
  181. background: ${theme.colors.background.primary};
  182. border: 1px solid ${theme.components.panel.borderColor};
  183. border-top: none;
  184. flex-grow: 1;
  185. `,
  186. });