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