getFieldOverrideElements.tsx 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. import { css } from '@emotion/css';
  2. import { cloneDeep } from 'lodash';
  3. import React from 'react';
  4. import {
  5. FieldConfigOptionsRegistry,
  6. SelectableValue,
  7. isSystemOverride as isSystemOverrideGuard,
  8. VariableSuggestionsScope,
  9. DynamicConfigValue,
  10. ConfigOverrideRule,
  11. GrafanaTheme2,
  12. } from '@grafana/data';
  13. import { fieldMatchersUI, useStyles2, ValuePicker } from '@grafana/ui';
  14. import { getDataLinksVariableSuggestions } from 'app/features/panel/panellinks/link_srv';
  15. import { DynamicConfigValueEditor } from './DynamicConfigValueEditor';
  16. import { OptionsPaneCategoryDescriptor } from './OptionsPaneCategoryDescriptor';
  17. import { OptionsPaneItemDescriptor } from './OptionsPaneItemDescriptor';
  18. import { OverrideCategoryTitle } from './OverrideCategoryTitle';
  19. import { OptionPaneRenderProps } from './types';
  20. export function getFieldOverrideCategories(
  21. props: OptionPaneRenderProps,
  22. searchQuery: string
  23. ): OptionsPaneCategoryDescriptor[] {
  24. const categories: OptionsPaneCategoryDescriptor[] = [];
  25. const currentFieldConfig = props.panel.fieldConfig;
  26. const registry = props.plugin.fieldConfigRegistry;
  27. const data = props.data?.series ?? [];
  28. if (registry.isEmpty()) {
  29. return [];
  30. }
  31. const onOverrideChange = (index: number, override: any) => {
  32. let overrides = cloneDeep(currentFieldConfig.overrides);
  33. overrides[index] = override;
  34. props.onFieldConfigsChange({ ...currentFieldConfig, overrides });
  35. };
  36. const onOverrideRemove = (overrideIndex: number) => {
  37. let overrides = cloneDeep(currentFieldConfig.overrides);
  38. overrides.splice(overrideIndex, 1);
  39. props.onFieldConfigsChange({ ...currentFieldConfig, overrides });
  40. };
  41. const onOverrideAdd = (value: SelectableValue<string>) => {
  42. props.onFieldConfigsChange({
  43. ...currentFieldConfig,
  44. overrides: [
  45. ...currentFieldConfig.overrides,
  46. {
  47. matcher: {
  48. id: value.value!,
  49. },
  50. properties: [],
  51. },
  52. ],
  53. });
  54. };
  55. const context = {
  56. data,
  57. getSuggestions: (scope?: VariableSuggestionsScope) => getDataLinksVariableSuggestions(data, scope),
  58. isOverride: true,
  59. };
  60. /**
  61. * Main loop through all override rules
  62. */
  63. for (let idx = 0; idx < currentFieldConfig.overrides.length; idx++) {
  64. const override = currentFieldConfig.overrides[idx];
  65. const overrideName = `Override ${idx + 1}`;
  66. const matcherUi = fieldMatchersUI.get(override.matcher.id);
  67. const configPropertiesOptions = getOverrideProperties(registry);
  68. const isSystemOverride = isSystemOverrideGuard(override);
  69. // A way to force open new override categories
  70. const forceOpen = override.properties.length === 0 ? 1 : 0;
  71. const category = new OptionsPaneCategoryDescriptor({
  72. title: overrideName,
  73. id: overrideName,
  74. forceOpen,
  75. renderTitle: function renderOverrideTitle(isExpanded: boolean) {
  76. return (
  77. <OverrideCategoryTitle
  78. override={override}
  79. isExpanded={isExpanded}
  80. registry={registry}
  81. overrideName={overrideName}
  82. matcherUi={matcherUi}
  83. onOverrideRemove={() => onOverrideRemove(idx)}
  84. />
  85. );
  86. },
  87. });
  88. const onMatcherConfigChange = (options: any) => {
  89. override.matcher.options = options;
  90. onOverrideChange(idx, override);
  91. };
  92. const onDynamicConfigValueAdd = (o: ConfigOverrideRule, value: SelectableValue<string>) => {
  93. const registryItem = registry.get(value.value!);
  94. const propertyConfig: DynamicConfigValue = {
  95. id: registryItem.id,
  96. value: registryItem.defaultValue,
  97. };
  98. if (override.properties) {
  99. o.properties.push(propertyConfig);
  100. } else {
  101. o.properties = [propertyConfig];
  102. }
  103. onOverrideChange(idx, o);
  104. };
  105. /**
  106. * Add override matcher UI element
  107. */
  108. category.addItem(
  109. new OptionsPaneItemDescriptor({
  110. title: matcherUi.name,
  111. render: function renderMatcherUI() {
  112. return (
  113. <matcherUi.component
  114. id={`${matcherUi.matcher.id}-${idx}`}
  115. matcher={matcherUi.matcher}
  116. data={props.data?.series ?? []}
  117. options={override.matcher.options}
  118. onChange={onMatcherConfigChange}
  119. />
  120. );
  121. },
  122. })
  123. );
  124. /**
  125. * Loop through all override properties
  126. */
  127. for (let propIdx = 0; propIdx < override.properties.length; propIdx++) {
  128. const property = override.properties[propIdx];
  129. const registryItemForProperty = registry.getIfExists(property.id);
  130. if (!registryItemForProperty) {
  131. continue;
  132. }
  133. const onPropertyChange = (value: any) => {
  134. override.properties[propIdx].value = value;
  135. onOverrideChange(idx, override);
  136. };
  137. const onPropertyRemove = () => {
  138. override.properties.splice(propIdx, 1);
  139. onOverrideChange(idx, override);
  140. };
  141. /**
  142. * Add override property item
  143. */
  144. category.addItem(
  145. new OptionsPaneItemDescriptor({
  146. title: registryItemForProperty.name,
  147. skipField: true,
  148. render: function renderPropertyEditor() {
  149. return (
  150. <DynamicConfigValueEditor
  151. key={`${property.id}/${propIdx}`}
  152. isSystemOverride={isSystemOverride}
  153. onChange={onPropertyChange}
  154. onRemove={onPropertyRemove}
  155. property={property}
  156. registry={registry}
  157. context={context}
  158. searchQuery={searchQuery}
  159. />
  160. );
  161. },
  162. })
  163. );
  164. }
  165. /**
  166. * Add button that adds new overrides
  167. */
  168. if (!isSystemOverride && override.matcher.options) {
  169. category.addItem(
  170. new OptionsPaneItemDescriptor({
  171. title: '----------',
  172. skipField: true,
  173. render: function renderAddPropertyButton() {
  174. return (
  175. <ValuePicker
  176. key="Add override property"
  177. label="Add override property"
  178. variant="secondary"
  179. isFullWidth={true}
  180. icon="plus"
  181. menuPlacement="auto"
  182. options={configPropertiesOptions}
  183. onChange={(v) => onDynamicConfigValueAdd(override, v)}
  184. />
  185. );
  186. },
  187. })
  188. );
  189. }
  190. categories.push(category);
  191. }
  192. categories.push(
  193. new OptionsPaneCategoryDescriptor({
  194. title: 'add button',
  195. id: 'add button',
  196. customRender: function renderAddButton() {
  197. return (
  198. <AddOverrideButtonContainer key="Add override">
  199. <ValuePicker
  200. icon="plus"
  201. label="Add field override"
  202. variant="secondary"
  203. menuPlacement="auto"
  204. isFullWidth={true}
  205. size="md"
  206. options={fieldMatchersUI
  207. .list()
  208. .filter((o) => !o.excludeFromPicker)
  209. .map<SelectableValue<string>>((i) => ({ label: i.name, value: i.id, description: i.description }))}
  210. onChange={(value) => onOverrideAdd(value)}
  211. />
  212. </AddOverrideButtonContainer>
  213. );
  214. },
  215. })
  216. );
  217. return categories;
  218. }
  219. function getOverrideProperties(registry: FieldConfigOptionsRegistry) {
  220. return registry
  221. .list()
  222. .filter((o) => !o.hideFromOverrides)
  223. .map((item) => {
  224. let label = item.name;
  225. if (item.category) {
  226. label = [...item.category, item.name].join(' > ');
  227. }
  228. return {
  229. label,
  230. value: item.id,
  231. description: item.description,
  232. };
  233. });
  234. }
  235. function AddOverrideButtonContainer({ children }: { children: React.ReactNode }) {
  236. const styles = useStyles2(getBorderTopStyles);
  237. return <div className={styles}>{children}</div>;
  238. }
  239. function getBorderTopStyles(theme: GrafanaTheme2) {
  240. return css({
  241. borderTop: `1px solid ${theme.colors.border.weak}`,
  242. padding: `${theme.spacing(2)}`,
  243. display: 'flex',
  244. });
  245. }