1import {
2  FieldConfigSource,
3  GrafanaPlugin,
4  PanelEditorProps,
5  PanelMigrationHandler,
6  PanelPluginMeta,
7  PanelProps,
8  PanelTypeChangedHandler,
9  FieldConfigProperty,
10  PanelPluginDataSupport,
11  VisualizationSuggestionsSupplier,
12} from '../types';
13import { FieldConfigEditorBuilder, PanelOptionsEditorBuilder } from '../utils/OptionsUIBuilders';
14import { ComponentClass, ComponentType } from 'react';
15import { set } from 'lodash';
16import { deprecationWarning } from '../utils';
17import { FieldConfigOptionsRegistry, StandardEditorContext } from '../field';
18import { createFieldConfigRegistry } from './registryFactories';
19
20/** @beta */
21export type StandardOptionConfig = {
22  defaultValue?: any;
23  settings?: any;
24};
25
26/** @beta */
27export interface SetFieldConfigOptionsArgs<TFieldConfigOptions = any> {
28  /**
29   * Configuration object of the standard field config properites
30   *
31   * @example
32   * ```typescript
33   * {
34   *   standardOptions: {
35   *     [FieldConfigProperty.Decimals]: {
36   *       defaultValue: 3
37   *     }
38   *   }
39   * }
40   * ```
41   */
42  standardOptions?: Partial<Record<FieldConfigProperty, StandardOptionConfig>>;
43
44  /**
45   * Array of standard field config properties that should not be available in the panel
46   * @example
47   * ```typescript
48   * {
49   *   disableStandardOptions: [FieldConfigProperty.Min, FieldConfigProperty.Max, FieldConfigProperty.Unit]
50   * }
51   * ```
52   */
53  disableStandardOptions?: FieldConfigProperty[];
54
55  /**
56   * Function that allows custom field config properties definition.
57   *
58   * @param builder
59   *
60   * @example
61   * ```typescript
62   * useCustomConfig: builder => {
63   *   builder
64   *    .addNumberInput({
65   *      id: 'shapeBorderWidth',
66   *      name: 'Border width',
67   *      description: 'Border width of the shape',
68   *      settings: {
69   *        min: 1,
70   *        max: 5,
71   *      },
72   *    })
73   *    .addSelect({
74   *      id: 'displayMode',
75   *      name: 'Display mode',
76   *      description: 'How the shape shout be rendered'
77   *      settings: {
78   *      options: [{value: 'fill', label: 'Fill' }, {value: 'transparent', label: 'Transparent }]
79   *    },
80   *  })
81   * }
82   * ```
83   */
84  useCustomConfig?: (builder: FieldConfigEditorBuilder<TFieldConfigOptions>) => void;
85}
86
87export type PanelOptionsSupplier<TOptions> = (
88  builder: PanelOptionsEditorBuilder<TOptions>,
89  context: StandardEditorContext<TOptions>
90) => void;
91
92export class PanelPlugin<
93  TOptions = any,
94  TFieldConfigOptions extends object = any
95> extends GrafanaPlugin<PanelPluginMeta> {
96  private _defaults?: TOptions;
97  private _fieldConfigDefaults: FieldConfigSource<TFieldConfigOptions> = {
98    defaults: {},
99    overrides: [],
100  };
101
102  private _fieldConfigRegistry?: FieldConfigOptionsRegistry;
103  private _initConfigRegistry = () => {
104    return new FieldConfigOptionsRegistry();
105  };
106
107  private optionsSupplier?: PanelOptionsSupplier<TOptions>;
108  private suggestionsSupplier?: VisualizationSuggestionsSupplier;
109
110  panel: ComponentType<PanelProps<TOptions>> | null;
111  editor?: ComponentClass<PanelEditorProps<TOptions>>;
112  onPanelMigration?: PanelMigrationHandler<TOptions>;
113  onPanelTypeChanged?: PanelTypeChangedHandler<TOptions>;
114  noPadding?: boolean;
115  dataSupport: PanelPluginDataSupport = {
116    annotations: false,
117    alertStates: false,
118  };
119
120  /**
121   * Legacy angular ctrl.  If this exists it will be used instead of the panel
122   */
123  angularPanelCtrl?: any;
124
125  constructor(panel: ComponentType<PanelProps<TOptions>> | null) {
126    super();
127    this.panel = panel;
128  }
129
130  get defaults() {
131    let result = this._defaults || {};
132
133    if (!this._defaults && this.optionsSupplier) {
134      const builder = new PanelOptionsEditorBuilder<TOptions>();
135      this.optionsSupplier(builder, { data: [] });
136      for (const item of builder.getItems()) {
137        if (item.defaultValue != null) {
138          set(result, item.path, item.defaultValue);
139        }
140      }
141    }
142
143    return result;
144  }
145
146  get fieldConfigDefaults(): FieldConfigSource<TFieldConfigOptions> {
147    const configDefaults = this._fieldConfigDefaults.defaults;
148    configDefaults.custom = {} as TFieldConfigOptions;
149
150    for (const option of this.fieldConfigRegistry.list()) {
151      if (option.defaultValue === undefined) {
152        continue;
153      }
154
155      set(configDefaults, option.id, option.defaultValue);
156    }
157
158    return {
159      defaults: {
160        ...configDefaults,
161      },
162      overrides: this._fieldConfigDefaults.overrides,
163    };
164  }
165
166  /**
167   * @deprecated setDefaults is deprecated in favor of setPanelOptions
168   */
169  setDefaults(defaults: TOptions) {
170    deprecationWarning('PanelPlugin', 'setDefaults', 'setPanelOptions');
171    this._defaults = defaults;
172    return this;
173  }
174
175  get fieldConfigRegistry() {
176    if (!this._fieldConfigRegistry) {
177      this._fieldConfigRegistry = this._initConfigRegistry();
178    }
179
180    return this._fieldConfigRegistry;
181  }
182
183  /**
184   * @deprecated setEditor is deprecated in favor of setPanelOptions
185   */
186  setEditor(editor: ComponentClass<PanelEditorProps<TOptions>>) {
187    deprecationWarning('PanelPlugin', 'setEditor', 'setPanelOptions');
188    this.editor = editor;
189    return this;
190  }
191
192  setNoPadding() {
193    this.noPadding = true;
194    return this;
195  }
196
197  /**
198   * This function is called before the panel first loads if
199   * the current version is different than the version that was saved.
200   *
201   * This is a good place to support any changes to the options model
202   */
203  setMigrationHandler(handler: PanelMigrationHandler<TOptions>) {
204    this.onPanelMigration = handler;
205    return this;
206  }
207
208  /**
209   * This function is called when the visualization was changed. This
210   * passes in the panel model for previous visualisation options inspection
211   * and panel model updates.
212   *
213   * This is useful for supporting PanelModel API updates when changing
214   * between Angular and React panels.
215   */
216  setPanelChangeHandler(handler: PanelTypeChangedHandler) {
217    this.onPanelTypeChanged = handler;
218    return this;
219  }
220
221  /**
222   * Enables panel options editor creation
223   *
224   * @example
225   * ```typescript
226   *
227   * import { ShapePanel } from './ShapePanel';
228   *
229   * interface ShapePanelOptions {}
230   *
231   * export const plugin = new PanelPlugin<ShapePanelOptions>(ShapePanel)
232   *   .setPanelOptions(builder => {
233   *     builder
234   *       .addSelect({
235   *         id: 'shape',
236   *         name: 'Shape',
237   *         description: 'Select shape to render'
238   *         settings: {
239   *           options: [
240   *             {value: 'circle', label: 'Circle' },
241   *             {value: 'square', label: 'Square },
242   *             {value: 'triangle', label: 'Triangle }
243   *            ]
244   *         },
245   *       })
246   *   })
247   * ```
248   *
249   * @public
250   **/
251  setPanelOptions(builder: PanelOptionsSupplier<TOptions>) {
252    // builder is applied lazily when options UI is created
253    this.optionsSupplier = builder;
254    return this;
255  }
256
257  /**
258   * This is used while building the panel options editor.
259   *
260   * @internal
261   */
262  getPanelOptionsSupplier(): PanelOptionsSupplier<TOptions> {
263    return this.optionsSupplier ?? ((() => {}) as PanelOptionsSupplier<TOptions>);
264  }
265
266  /**
267   * Tells Grafana if the plugin should subscribe to annotation and alertState results.
268   *
269   * @example
270   * ```typescript
271   *
272   * import { ShapePanel } from './ShapePanel';
273   *
274   * interface ShapePanelOptions {}
275   *
276   * export const plugin = new PanelPlugin<ShapePanelOptions>(ShapePanel)
277   *     .useFieldConfig({})
278   *     ...
279   *     ...
280   *     .setDataSupport({
281   *       annotations: true,
282   *       alertStates: true,
283   *     });
284   * ```
285   *
286   * @public
287   **/
288  setDataSupport(support: Partial<PanelPluginDataSupport>) {
289    this.dataSupport = { ...this.dataSupport, ...support };
290    return this;
291  }
292
293  /**
294   * Allows specifying which standard field config options panel should use and defining default values
295   *
296   * @example
297   * ```typescript
298   *
299   * import { ShapePanel } from './ShapePanel';
300   *
301   * interface ShapePanelOptions {}
302   *
303   * // when plugin should use all standard options
304   * export const plugin = new PanelPlugin<ShapePanelOptions>(ShapePanel)
305   *  .useFieldConfig();
306   *
307   * // when plugin should only display specific standard options
308   * // note, that options will be displayed in the order they are provided
309   * export const plugin = new PanelPlugin<ShapePanelOptions>(ShapePanel)
310   *  .useFieldConfig({
311   *    standardOptions: [FieldConfigProperty.Min, FieldConfigProperty.Max]
312   *   });
313   *
314   * // when standard option's default value needs to be provided
315   * export const plugin = new PanelPlugin<ShapePanelOptions>(ShapePanel)
316   *  .useFieldConfig({
317   *    standardOptions: [FieldConfigProperty.Min, FieldConfigProperty.Max],
318   *    standardOptionsDefaults: {
319   *      [FieldConfigProperty.Min]: 20,
320   *      [FieldConfigProperty.Max]: 100
321   *    }
322   *  });
323   *
324   * // when custom field config options needs to be provided
325   * export const plugin = new PanelPlugin<ShapePanelOptions>(ShapePanel)
326   *  .useFieldConfig({
327   *    useCustomConfig: builder => {
328   *      builder
329   *       .addNumberInput({
330   *         id: 'shapeBorderWidth',
331   *         name: 'Border width',
332   *         description: 'Border width of the shape',
333   *         settings: {
334   *           min: 1,
335   *           max: 5,
336   *         },
337   *       })
338   *       .addSelect({
339   *         id: 'displayMode',
340   *         name: 'Display mode',
341   *         description: 'How the shape shout be rendered'
342   *         settings: {
343   *         options: [{value: 'fill', label: 'Fill' }, {value: 'transparent', label: 'Transparent }]
344   *       },
345   *     })
346   *   },
347   *  });
348   *
349   * ```
350   *
351   * @public
352   */
353  useFieldConfig(config: SetFieldConfigOptionsArgs<TFieldConfigOptions> = {}) {
354    // builder is applied lazily when custom field configs are accessed
355    this._initConfigRegistry = () => createFieldConfigRegistry(config, this.meta.name);
356
357    return this;
358  }
359
360  /**
361   * Sets function that can return visualization examples and suggestions.
362   * @alpha
363   */
364  setSuggestionsSupplier(supplier: VisualizationSuggestionsSupplier) {
365    this.suggestionsSupplier = supplier;
366    return this;
367  }
368
369  /**
370   * Returns the suggestions supplier
371   * @alpha
372   */
373  getSuggestionsSupplier(): VisualizationSuggestionsSupplier | undefined {
374    return this.suggestionsSupplier;
375  }
376}
377