mirror of https://github.com/grafana/grafana
Panel Options: support dynamic options editors (#39491)
parent
3a8d04603f
commit
3db98f417d
@ -1,123 +0,0 @@ |
||||
import React, { FC, useMemo } from 'react'; |
||||
import { Select } from '@grafana/ui'; |
||||
import { DataFrame, PanelOptionsEditorBuilder, StandardEditorContext } from '@grafana/data'; |
||||
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor'; |
||||
import { setOptionImmutably } from 'app/features/dashboard/components/PanelEditor/utils'; |
||||
import { fillOptionsPaneItems } from 'app/features/dashboard/components/PanelEditor/getVizualizationOptions'; |
||||
import { cloneDeep } from 'lodash'; |
||||
import { addBackgroundOptions, addBorderOptions } from './options'; |
||||
import { |
||||
CanvasElementItem, |
||||
CanvasElementOptions, |
||||
canvasElementRegistry, |
||||
DEFAULT_CANVAS_ELEMENT_CONFIG, |
||||
} from 'app/features/canvas'; |
||||
|
||||
export interface CanvasElementEditorProps<TConfig = any> { |
||||
options?: CanvasElementOptions<TConfig>; |
||||
data: DataFrame[]; // All results
|
||||
onChange: (options: CanvasElementOptions<TConfig>) => void; |
||||
filter?: (item: CanvasElementItem) => boolean; |
||||
} |
||||
|
||||
export const CanvasElementEditor: FC<CanvasElementEditorProps> = ({ options, onChange, data, filter }) => { |
||||
// all basemaps
|
||||
const layerTypes = useMemo(() => { |
||||
return canvasElementRegistry.selectOptions( |
||||
options?.type // the selected value
|
||||
? [options.type] // as an array
|
||||
: [DEFAULT_CANVAS_ELEMENT_CONFIG.type], |
||||
filter |
||||
); |
||||
}, [options?.type, filter]); |
||||
|
||||
// The options change with each layer type
|
||||
const optionsEditorBuilder = useMemo(() => { |
||||
const layer = canvasElementRegistry.getIfExists(options?.type); |
||||
if (!layer || !layer.registerOptionsUI) { |
||||
return null; |
||||
} |
||||
|
||||
const builder = new PanelOptionsEditorBuilder<CanvasElementOptions>(); |
||||
if (layer.registerOptionsUI) { |
||||
layer.registerOptionsUI(builder); |
||||
} |
||||
|
||||
addBackgroundOptions(builder); |
||||
addBorderOptions(builder); |
||||
return builder; |
||||
}, [options?.type]); |
||||
|
||||
// The react componnets
|
||||
const layerOptions = useMemo(() => { |
||||
const layer = canvasElementRegistry.getIfExists(options?.type); |
||||
if (!optionsEditorBuilder || !layer) { |
||||
return null; |
||||
} |
||||
|
||||
const category = new OptionsPaneCategoryDescriptor({ |
||||
id: 'CanvasElement config', |
||||
title: 'CanvasElement config', |
||||
}); |
||||
|
||||
const context: StandardEditorContext<any> = { |
||||
data, |
||||
options: options, |
||||
}; |
||||
|
||||
const currentOptions = { ...options, type: layer.id, config: { ...layer.defaultConfig, ...options?.config } }; |
||||
|
||||
// Update the panel options if not set
|
||||
if (!options || (layer.defaultConfig && !options.config)) { |
||||
onChange(currentOptions as any); |
||||
} |
||||
|
||||
const reg = optionsEditorBuilder.getRegistry(); |
||||
|
||||
// Load the options into categories
|
||||
fillOptionsPaneItems( |
||||
reg.list(), |
||||
|
||||
// Always use the same category
|
||||
(categoryNames) => category, |
||||
|
||||
// Custom upate function
|
||||
(path: string, value: any) => { |
||||
onChange(setOptionImmutably(currentOptions, path, value) as any); |
||||
}, |
||||
context |
||||
); |
||||
|
||||
return ( |
||||
<> |
||||
<br /> |
||||
{category.items.map((item) => item.render())} |
||||
</> |
||||
); |
||||
}, [optionsEditorBuilder, onChange, data, options]); |
||||
|
||||
return ( |
||||
<div> |
||||
<Select |
||||
menuShouldPortal |
||||
options={layerTypes.options} |
||||
value={layerTypes.current} |
||||
onChange={(v) => { |
||||
const layer = canvasElementRegistry.getIfExists(v.value); |
||||
if (!layer) { |
||||
console.warn('layer does not exist', v); |
||||
return; |
||||
} |
||||
|
||||
onChange({ |
||||
...options, // keep current options
|
||||
type: layer.id, |
||||
config: cloneDeep(layer.defaultConfig ?? {}), |
||||
}); |
||||
}} |
||||
/> |
||||
|
||||
{layerOptions} |
||||
</div> |
||||
); |
||||
}; |
||||
@ -1,27 +0,0 @@ |
||||
import React, { FC } from 'react'; |
||||
import { StandardEditorProps } from '@grafana/data'; |
||||
import { PanelOptions } from '../models.gen'; |
||||
import { CanvasElementEditor } from './ElementEditor'; |
||||
import { theScene } from '../CanvasPanel'; |
||||
import { useObservable } from 'react-use'; |
||||
import { of } from 'rxjs'; |
||||
import { CanvasGroupOptions } from 'app/features/canvas'; |
||||
|
||||
export const SelectedElementEditor: FC<StandardEditorProps<CanvasGroupOptions, any, PanelOptions>> = ({ context }) => { |
||||
const scene = useObservable(theScene); |
||||
const selected = useObservable(scene?.selected ?? of(undefined)); |
||||
|
||||
if (!selected) { |
||||
return <div>No item is selected</div>; |
||||
} |
||||
|
||||
return ( |
||||
<CanvasElementEditor |
||||
options={selected.options} |
||||
data={context.data} |
||||
onChange={(cfg) => { |
||||
scene!.onChange(selected.UID, cfg); |
||||
}} |
||||
/> |
||||
); |
||||
}; |
||||
@ -0,0 +1,75 @@ |
||||
import { cloneDeep, get as lodashGet } from 'lodash'; |
||||
import { optionBuilder } from './options'; |
||||
import { CanvasElementOptions, canvasElementRegistry, DEFAULT_CANVAS_ELEMENT_CONFIG } from 'app/features/canvas'; |
||||
import { NestedPanelOptions, NestedValueAccess } from '@grafana/data/src/utils/OptionsUIBuilders'; |
||||
import { setOptionImmutably } from 'app/features/dashboard/components/PanelEditor/utils'; |
||||
import { ElementState } from 'app/features/canvas/runtime/element'; |
||||
import { Scene } from 'app/features/canvas/runtime/scene'; |
||||
|
||||
export interface CanvasEditorOptions { |
||||
element: ElementState; |
||||
scene: Scene; |
||||
category?: string[]; |
||||
} |
||||
|
||||
export function getElementEditor(opts: CanvasEditorOptions): NestedPanelOptions<CanvasElementOptions> { |
||||
return { |
||||
category: opts.category, |
||||
path: '--', // not used!
|
||||
|
||||
// Note that canvas editor writes things to the scene!
|
||||
values: (parent: NestedValueAccess) => ({ |
||||
getValue: (path: string) => { |
||||
return lodashGet(opts.element.options, path); |
||||
}, |
||||
onChange: (path: string, value: any) => { |
||||
let options = opts.element.options; |
||||
if (path === 'type' && value) { |
||||
const layer = canvasElementRegistry.getIfExists(value); |
||||
if (!layer) { |
||||
console.warn('layer does not exist', value); |
||||
return; |
||||
} |
||||
options = { |
||||
...options, // keep current options
|
||||
type: layer.id, |
||||
config: cloneDeep(layer.defaultConfig ?? {}), |
||||
}; |
||||
} else { |
||||
options = setOptionImmutably(options, path, value); |
||||
} |
||||
opts.scene.onChange(opts.element.UID, options); |
||||
}, |
||||
}), |
||||
|
||||
// Dynamically fill the selected element
|
||||
build: (builder, context) => { |
||||
const { options } = opts.element; |
||||
const layerTypes = canvasElementRegistry.selectOptions( |
||||
options?.type // the selected value
|
||||
? [options.type] // as an array
|
||||
: [DEFAULT_CANVAS_ELEMENT_CONFIG.type] |
||||
); |
||||
|
||||
builder.addSelect({ |
||||
path: 'type', |
||||
name: undefined as any, // required, but hide space
|
||||
settings: { |
||||
options: layerTypes.options, |
||||
}, |
||||
}); |
||||
|
||||
// force clean layer configuration
|
||||
const layer = canvasElementRegistry.getIfExists(options?.type ?? DEFAULT_CANVAS_ELEMENT_CONFIG.type)!; |
||||
const currentOptions = { ...options, type: layer.id, config: { ...layer.defaultConfig, ...options?.config } }; |
||||
const ctx = { ...context, options: currentOptions }; |
||||
|
||||
if (layer.registerOptionsUI) { |
||||
layer.registerOptionsUI(builder, ctx); |
||||
} |
||||
|
||||
optionBuilder.addBackground(builder, ctx); |
||||
optionBuilder.addBorder(builder, ctx); |
||||
}, |
||||
}; |
||||
} |
||||
@ -1,66 +1,81 @@ |
||||
import { PanelOptionsEditorBuilder } from '@grafana/data'; |
||||
import { BackgroundImageSize } from 'app/features/canvas'; |
||||
import { PanelOptionsSupplier } from '@grafana/data/src/panel/PanelPlugin'; |
||||
import { BackgroundImageSize, CanvasElementOptions } from 'app/features/canvas'; |
||||
import { ColorDimensionEditor, ResourceDimensionEditor } from 'app/features/dimensions/editors'; |
||||
|
||||
export function addBackgroundOptions(builder: PanelOptionsEditorBuilder<any>) { |
||||
builder |
||||
.addCustomEditor({ |
||||
id: 'background.color', |
||||
path: 'background.color', |
||||
name: 'Background Color', |
||||
editor: ColorDimensionEditor, |
||||
settings: {}, |
||||
defaultValue: { |
||||
// Configured values
|
||||
fixed: '', |
||||
}, |
||||
}) |
||||
.addCustomEditor({ |
||||
id: 'background.image', |
||||
path: 'background.image', |
||||
name: 'Background Image', |
||||
editor: ResourceDimensionEditor, |
||||
settings: { |
||||
resourceType: 'image', |
||||
}, |
||||
}) |
||||
.addRadio({ |
||||
path: 'background.size', |
||||
name: 'Backround image size', |
||||
interface OptionSuppliers { |
||||
addBackground: PanelOptionsSupplier<CanvasElementOptions>; |
||||
addBorder: PanelOptionsSupplier<CanvasElementOptions>; |
||||
} |
||||
|
||||
export const optionBuilder: OptionSuppliers = { |
||||
addBackground: (builder, context) => { |
||||
const category = ['Background']; |
||||
builder |
||||
.addCustomEditor({ |
||||
category, |
||||
id: 'background.color', |
||||
path: 'background.color', |
||||
name: 'Color', |
||||
editor: ColorDimensionEditor, |
||||
settings: {}, |
||||
defaultValue: { |
||||
// Configured values
|
||||
fixed: '', |
||||
}, |
||||
}) |
||||
.addCustomEditor({ |
||||
category, |
||||
id: 'background.image', |
||||
path: 'background.image', |
||||
name: 'Image', |
||||
editor: ResourceDimensionEditor, |
||||
settings: { |
||||
resourceType: 'image', |
||||
}, |
||||
}) |
||||
.addRadio({ |
||||
category, |
||||
path: 'background.size', |
||||
name: 'Image size', |
||||
settings: { |
||||
options: [ |
||||
{ value: BackgroundImageSize.Original, label: 'Original' }, |
||||
{ value: BackgroundImageSize.Contain, label: 'Contain' }, |
||||
{ value: BackgroundImageSize.Cover, label: 'Cover' }, |
||||
{ value: BackgroundImageSize.Fill, label: 'Fill' }, |
||||
{ value: BackgroundImageSize.Tile, label: 'Tile' }, |
||||
], |
||||
}, |
||||
defaultValue: BackgroundImageSize.Cover, |
||||
}); |
||||
}, |
||||
|
||||
addBorder: (builder, context) => { |
||||
const category = ['Border']; |
||||
builder.addSliderInput({ |
||||
category, |
||||
path: 'border.width', |
||||
name: 'Width', |
||||
defaultValue: 2, |
||||
settings: { |
||||
options: [ |
||||
{ value: BackgroundImageSize.Original, label: 'Original' }, |
||||
{ value: BackgroundImageSize.Contain, label: 'Contain' }, |
||||
{ value: BackgroundImageSize.Cover, label: 'Cover' }, |
||||
{ value: BackgroundImageSize.Fill, label: 'Fill' }, |
||||
{ value: BackgroundImageSize.Tile, label: 'Tile' }, |
||||
], |
||||
min: 0, |
||||
max: 20, |
||||
}, |
||||
defaultValue: BackgroundImageSize.Cover, |
||||
}); |
||||
} |
||||
|
||||
export function addBorderOptions(builder: PanelOptionsEditorBuilder<any>) { |
||||
builder.addSliderInput({ |
||||
path: 'border.width', |
||||
name: 'Border Width', |
||||
defaultValue: 2, |
||||
settings: { |
||||
min: 0, |
||||
max: 20, |
||||
}, |
||||
}); |
||||
|
||||
builder.addCustomEditor({ |
||||
id: 'border.color', |
||||
path: 'border.color', |
||||
name: 'Border Color', |
||||
editor: ColorDimensionEditor, |
||||
settings: {}, |
||||
defaultValue: { |
||||
// Configured values
|
||||
fixed: '', |
||||
}, |
||||
showIf: (cfg) => Boolean(cfg.border?.width), |
||||
}); |
||||
} |
||||
if (context.options?.border?.width) { |
||||
builder.addCustomEditor({ |
||||
category, |
||||
id: 'border.color', |
||||
path: 'border.color', |
||||
name: 'Color', |
||||
editor: ColorDimensionEditor, |
||||
settings: {}, |
||||
defaultValue: { |
||||
// Configured values
|
||||
fixed: '', |
||||
}, |
||||
}); |
||||
} |
||||
}, |
||||
}; |
||||
|
||||
@ -1,19 +1,29 @@ |
||||
import { PanelPlugin } from '@grafana/data'; |
||||
|
||||
import { CanvasPanel } from './CanvasPanel'; |
||||
import { SelectedElementEditor } from './editor/SelectedElementEditor'; |
||||
import { defaultPanelOptions, PanelOptions } from './models.gen'; |
||||
import { CanvasPanel, InstanceState } from './CanvasPanel'; |
||||
import { PanelOptions } from './models.gen'; |
||||
import { getElementEditor } from './editor/elementEditor'; |
||||
|
||||
export const plugin = new PanelPlugin<PanelOptions>(CanvasPanel) |
||||
.setNoPadding() // extend to panel edges
|
||||
.useFieldConfig() |
||||
.setPanelOptions((builder) => { |
||||
builder.addCustomEditor({ |
||||
category: ['Selected Element'], |
||||
id: 'root', |
||||
path: 'root', // multiple elements may edit root!
|
||||
name: 'Selected Element', |
||||
editor: SelectedElementEditor, |
||||
defaultValue: defaultPanelOptions.root, |
||||
.setPanelOptions((builder, context) => { |
||||
const state: InstanceState = context.instanceState; |
||||
|
||||
builder.addBooleanSwitch({ |
||||
path: 'inlineEditing', |
||||
name: 'Inline editing', |
||||
description: 'Enable editing while the panel is in dashboard mode', |
||||
defaultValue: true, |
||||
}); |
||||
|
||||
if (state?.selected) { |
||||
builder.addNestedOptions( |
||||
getElementEditor({ |
||||
category: ['Selected element'], |
||||
element: state.selected, |
||||
scene: state.scene, |
||||
}) |
||||
); |
||||
} |
||||
}); |
||||
|
||||
@ -1,27 +0,0 @@ |
||||
import React, { FC } from 'react'; |
||||
import { StandardEditorProps, MapLayerOptions, MapLayerRegistryItem, PluginState } from '@grafana/data'; |
||||
import { GeomapPanelOptions } from '../types'; |
||||
import { LayerEditor } from './LayerEditor'; |
||||
import { config, hasAlphaPanels } from 'app/core/config'; |
||||
|
||||
function baseMapFilter(layer: MapLayerRegistryItem): boolean { |
||||
if (!layer.isBaseMap) { |
||||
return false; |
||||
} |
||||
if (layer.state === PluginState.alpha) { |
||||
return hasAlphaPanels; |
||||
} |
||||
return true; |
||||
} |
||||
|
||||
export const BaseLayerEditor: FC<StandardEditorProps<MapLayerOptions, any, GeomapPanelOptions>> = ({ |
||||
value, |
||||
onChange, |
||||
context, |
||||
}) => { |
||||
if (config.geomapDisableCustomBaseLayer) { |
||||
return <div>The base layer is configured by the server admin.</div>; |
||||
} |
||||
|
||||
return <LayerEditor options={value} data={context.data} onChange={onChange} filter={baseMapFilter} />; |
||||
}; |
||||
@ -1,34 +0,0 @@ |
||||
import React, { FC } from 'react'; |
||||
import { StandardEditorProps, MapLayerOptions, PluginState, MapLayerRegistryItem } from '@grafana/data'; |
||||
import { GeomapPanelOptions } from '../types'; |
||||
import { LayerEditor } from './LayerEditor'; |
||||
import { hasAlphaPanels } from 'app/core/config'; |
||||
|
||||
function dataLayerFilter(layer: MapLayerRegistryItem): boolean { |
||||
if (layer.isBaseMap) { |
||||
return false; |
||||
} |
||||
if (layer.state === PluginState.alpha) { |
||||
return hasAlphaPanels; |
||||
} |
||||
return true; |
||||
} |
||||
|
||||
// For now this supports a *single* data layer -- eventually we should support more than one
|
||||
export const DataLayersEditor: FC<StandardEditorProps<MapLayerOptions[], any, GeomapPanelOptions>> = ({ |
||||
value, |
||||
onChange, |
||||
context, |
||||
}) => { |
||||
return ( |
||||
<LayerEditor |
||||
options={value?.length ? value[0] : undefined} |
||||
data={context.data} |
||||
onChange={(cfg) => { |
||||
console.log('Change overlays:', cfg); |
||||
onChange([cfg]); |
||||
}} |
||||
filter={dataLayerFilter} |
||||
/> |
||||
); |
||||
}; |
||||
@ -1,188 +0,0 @@ |
||||
import React, { FC, useMemo } from 'react'; |
||||
import { Select } from '@grafana/ui'; |
||||
import { |
||||
MapLayerOptions, |
||||
DataFrame, |
||||
MapLayerRegistryItem, |
||||
PanelOptionsEditorBuilder, |
||||
StandardEditorContext, |
||||
FrameGeometrySourceMode, |
||||
FieldType, |
||||
Field, |
||||
} from '@grafana/data'; |
||||
import { DEFAULT_BASEMAP_CONFIG, geomapLayerRegistry } from '../layers/registry'; |
||||
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor'; |
||||
import { setOptionImmutably } from 'app/features/dashboard/components/PanelEditor/utils'; |
||||
import { fillOptionsPaneItems } from 'app/features/dashboard/components/PanelEditor/getVizualizationOptions'; |
||||
import { GazetteerPathEditor } from './GazetteerPathEditor'; |
||||
|
||||
export interface LayerEditorProps<TConfig = any> { |
||||
options?: MapLayerOptions<TConfig>; |
||||
data: DataFrame[]; // All results
|
||||
onChange: (options: MapLayerOptions<TConfig>) => void; |
||||
filter: (item: MapLayerRegistryItem) => boolean; |
||||
} |
||||
|
||||
export const LayerEditor: FC<LayerEditorProps> = ({ options, onChange, data, filter }) => { |
||||
// all basemaps
|
||||
const layerTypes = useMemo(() => { |
||||
return geomapLayerRegistry.selectOptions( |
||||
options?.type // the selected value
|
||||
? [options.type] // as an array
|
||||
: [DEFAULT_BASEMAP_CONFIG.type], |
||||
filter |
||||
); |
||||
}, [options?.type, filter]); |
||||
|
||||
// The options change with each layer type
|
||||
const optionsEditorBuilder = useMemo(() => { |
||||
const layer = geomapLayerRegistry.getIfExists(options?.type); |
||||
if (!layer || !(layer.registerOptionsUI || layer.showLocation || layer.showOpacity)) { |
||||
return null; |
||||
} |
||||
|
||||
const builder = new PanelOptionsEditorBuilder<MapLayerOptions>(); |
||||
if (layer.showLocation) { |
||||
builder |
||||
.addRadio({ |
||||
path: 'location.mode', |
||||
name: 'Location', |
||||
description: '', |
||||
defaultValue: FrameGeometrySourceMode.Auto, |
||||
settings: { |
||||
options: [ |
||||
{ value: FrameGeometrySourceMode.Auto, label: 'Auto' }, |
||||
{ value: FrameGeometrySourceMode.Coords, label: 'Coords' }, |
||||
{ value: FrameGeometrySourceMode.Geohash, label: 'Geohash' }, |
||||
{ value: FrameGeometrySourceMode.Lookup, label: 'Lookup' }, |
||||
], |
||||
}, |
||||
}) |
||||
.addFieldNamePicker({ |
||||
path: 'location.latitude', |
||||
name: 'Latitude field', |
||||
settings: { |
||||
filter: (f: Field) => f.type === FieldType.number, |
||||
noFieldsMessage: 'No numeric fields found', |
||||
}, |
||||
showIf: (opts) => opts.location?.mode === FrameGeometrySourceMode.Coords, |
||||
}) |
||||
.addFieldNamePicker({ |
||||
path: 'location.longitude', |
||||
name: 'Longitude field', |
||||
settings: { |
||||
filter: (f: Field) => f.type === FieldType.number, |
||||
noFieldsMessage: 'No numeric fields found', |
||||
}, |
||||
showIf: (opts) => opts.location?.mode === FrameGeometrySourceMode.Coords, |
||||
}) |
||||
.addFieldNamePicker({ |
||||
path: 'location.geohash', |
||||
name: 'Geohash field', |
||||
settings: { |
||||
filter: (f: Field) => f.type === FieldType.string, |
||||
noFieldsMessage: 'No strings fields found', |
||||
}, |
||||
showIf: (opts) => opts.location?.mode === FrameGeometrySourceMode.Geohash, |
||||
// eslint-disable-next-line react/display-name
|
||||
// info: (props) => <div>HELLO</div>,
|
||||
}) |
||||
.addFieldNamePicker({ |
||||
path: 'location.lookup', |
||||
name: 'Lookup field', |
||||
settings: { |
||||
filter: (f: Field) => f.type === FieldType.string, |
||||
noFieldsMessage: 'No strings fields found', |
||||
}, |
||||
showIf: (opts) => opts.location?.mode === FrameGeometrySourceMode.Lookup, |
||||
}) |
||||
.addCustomEditor({ |
||||
id: 'gazetteer', |
||||
path: 'location.gazetteer', |
||||
name: 'Gazetteer', |
||||
editor: GazetteerPathEditor, |
||||
showIf: (opts) => opts.location?.mode === FrameGeometrySourceMode.Lookup, |
||||
}); |
||||
} |
||||
if (layer.registerOptionsUI) { |
||||
layer.registerOptionsUI(builder); |
||||
} |
||||
if (layer.showOpacity) { |
||||
// TODO -- add opacity check
|
||||
} |
||||
return builder; |
||||
}, [options?.type]); |
||||
|
||||
// The react componnets
|
||||
const layerOptions = useMemo(() => { |
||||
const layer = geomapLayerRegistry.getIfExists(options?.type); |
||||
if (!optionsEditorBuilder || !layer) { |
||||
return null; |
||||
} |
||||
|
||||
const category = new OptionsPaneCategoryDescriptor({ |
||||
id: 'Layer config', |
||||
title: 'Layer config', |
||||
}); |
||||
|
||||
const context: StandardEditorContext<any> = { |
||||
data, |
||||
options: options, |
||||
}; |
||||
|
||||
const currentOptions = { ...options, type: layer.id, config: { ...layer.defaultOptions, ...options?.config } }; |
||||
|
||||
// Update the panel options if not set
|
||||
if (!options || (layer.defaultOptions && !options.config)) { |
||||
onChange(currentOptions as any); |
||||
} |
||||
|
||||
const reg = optionsEditorBuilder.getRegistry(); |
||||
|
||||
// Load the options into categories
|
||||
fillOptionsPaneItems( |
||||
reg.list(), |
||||
|
||||
// Always use the same category
|
||||
(categoryNames) => category, |
||||
|
||||
// Custom upate function
|
||||
(path: string, value: any) => { |
||||
onChange(setOptionImmutably(currentOptions, path, value) as any); |
||||
}, |
||||
context |
||||
); |
||||
|
||||
return ( |
||||
<> |
||||
<br /> |
||||
{category.items.map((item) => item.render())} |
||||
</> |
||||
); |
||||
}, [optionsEditorBuilder, onChange, data, options]); |
||||
|
||||
return ( |
||||
<div> |
||||
<Select |
||||
menuShouldPortal |
||||
options={layerTypes.options} |
||||
value={layerTypes.current} |
||||
onChange={(v) => { |
||||
const layer = geomapLayerRegistry.getIfExists(v.value); |
||||
if (!layer) { |
||||
console.warn('layer does not exist', v); |
||||
return; |
||||
} |
||||
|
||||
onChange({ |
||||
...options, // keep current options
|
||||
type: layer.id, |
||||
config: { ...layer.defaultOptions }, // clone?
|
||||
}); |
||||
}} |
||||
/> |
||||
|
||||
{layerOptions} |
||||
</div> |
||||
); |
||||
}; |
||||
@ -0,0 +1,155 @@ |
||||
import { |
||||
MapLayerOptions, |
||||
FrameGeometrySourceMode, |
||||
FieldType, |
||||
Field, |
||||
MapLayerRegistryItem, |
||||
PluginState, |
||||
} from '@grafana/data'; |
||||
import { DEFAULT_BASEMAP_CONFIG, geomapLayerRegistry } from '../layers/registry'; |
||||
import { GazetteerPathEditor } from './GazetteerPathEditor'; |
||||
import { NestedPanelOptions, NestedValueAccess } from '@grafana/data/src/utils/OptionsUIBuilders'; |
||||
import { defaultMarkersConfig } from '../layers/data/markersLayer'; |
||||
import { hasAlphaPanels } from 'app/core/config'; |
||||
|
||||
export interface LayerEditorOptions { |
||||
category: string[]; |
||||
path: string; |
||||
basemaps: boolean; // only basemaps
|
||||
current?: MapLayerOptions; |
||||
} |
||||
|
||||
export function getLayerEditor(opts: LayerEditorOptions): NestedPanelOptions<MapLayerOptions> { |
||||
return { |
||||
category: opts.category, |
||||
path: opts.path, |
||||
defaultValue: opts.basemaps ? DEFAULT_BASEMAP_CONFIG : defaultMarkersConfig, |
||||
values: (parent: NestedValueAccess) => ({ |
||||
getValue: (path: string) => parent.getValue(`${opts.path}.${path}`), |
||||
onChange: (path: string, value: any) => { |
||||
if (path === 'type' && value) { |
||||
const layer = geomapLayerRegistry.getIfExists(value); |
||||
if (layer) { |
||||
parent.onChange(opts.path, { |
||||
...opts.current, // keep current shared options
|
||||
type: layer.id, |
||||
config: { ...layer.defaultOptions }, // clone?
|
||||
}); |
||||
return; // reset current values
|
||||
} |
||||
} |
||||
parent.onChange(`${opts.path}.${path}`, value); |
||||
}, |
||||
}), |
||||
build: (builder, context) => { |
||||
const { options } = context; |
||||
const layer = geomapLayerRegistry.getIfExists(options?.type); |
||||
|
||||
const layerTypes = geomapLayerRegistry.selectOptions( |
||||
options?.type // the selected value
|
||||
? [options.type] // as an array
|
||||
: [DEFAULT_BASEMAP_CONFIG.type], |
||||
opts.basemaps ? baseMapFilter : dataLayerFilter |
||||
); |
||||
|
||||
builder.addSelect({ |
||||
path: 'type', |
||||
name: undefined as any, // required, but hide space
|
||||
settings: { |
||||
options: layerTypes.options, |
||||
}, |
||||
}); |
||||
|
||||
if (layer) { |
||||
if (layer.showLocation) { |
||||
builder |
||||
.addRadio({ |
||||
path: 'location.mode', |
||||
name: 'Location', |
||||
description: '', |
||||
defaultValue: FrameGeometrySourceMode.Auto, |
||||
settings: { |
||||
options: [ |
||||
{ value: FrameGeometrySourceMode.Auto, label: 'Auto' }, |
||||
{ value: FrameGeometrySourceMode.Coords, label: 'Coords' }, |
||||
{ value: FrameGeometrySourceMode.Geohash, label: 'Geohash' }, |
||||
{ value: FrameGeometrySourceMode.Lookup, label: 'Lookup' }, |
||||
], |
||||
}, |
||||
}) |
||||
.addFieldNamePicker({ |
||||
path: 'location.latitude', |
||||
name: 'Latitude field', |
||||
settings: { |
||||
filter: (f: Field) => f.type === FieldType.number, |
||||
noFieldsMessage: 'No numeric fields found', |
||||
}, |
||||
showIf: (opts) => opts.location?.mode === FrameGeometrySourceMode.Coords, |
||||
}) |
||||
.addFieldNamePicker({ |
||||
path: 'location.longitude', |
||||
name: 'Longitude field', |
||||
settings: { |
||||
filter: (f: Field) => f.type === FieldType.number, |
||||
noFieldsMessage: 'No numeric fields found', |
||||
}, |
||||
showIf: (opts) => opts.location?.mode === FrameGeometrySourceMode.Coords, |
||||
}) |
||||
.addFieldNamePicker({ |
||||
path: 'location.geohash', |
||||
name: 'Geohash field', |
||||
settings: { |
||||
filter: (f: Field) => f.type === FieldType.string, |
||||
noFieldsMessage: 'No strings fields found', |
||||
}, |
||||
showIf: (opts) => opts.location?.mode === FrameGeometrySourceMode.Geohash, |
||||
// eslint-disable-next-line react/display-name
|
||||
// info: (props) => <div>HELLO</div>,
|
||||
}) |
||||
.addFieldNamePicker({ |
||||
path: 'location.lookup', |
||||
name: 'Lookup field', |
||||
settings: { |
||||
filter: (f: Field) => f.type === FieldType.string, |
||||
noFieldsMessage: 'No strings fields found', |
||||
}, |
||||
showIf: (opts) => opts.location?.mode === FrameGeometrySourceMode.Lookup, |
||||
}) |
||||
.addCustomEditor({ |
||||
id: 'gazetteer', |
||||
path: 'location.gazetteer', |
||||
name: 'Gazetteer', |
||||
editor: GazetteerPathEditor, |
||||
showIf: (opts) => opts.location?.mode === FrameGeometrySourceMode.Lookup, |
||||
}); |
||||
} |
||||
if (layer.registerOptionsUI) { |
||||
layer.registerOptionsUI(builder); |
||||
} |
||||
if (layer.showOpacity) { |
||||
// TODO -- add opacity check
|
||||
} |
||||
} |
||||
}, |
||||
}; |
||||
} |
||||
|
||||
function baseMapFilter(layer: MapLayerRegistryItem): boolean { |
||||
if (!layer.isBaseMap) { |
||||
return false; |
||||
} |
||||
if (layer.state === PluginState.alpha) { |
||||
return hasAlphaPanels; |
||||
} |
||||
return true; |
||||
} |
||||
|
||||
function dataLayerFilter(layer: MapLayerRegistryItem): boolean { |
||||
if (layer.isBaseMap) { |
||||
return false; |
||||
} |
||||
if (layer.state === PluginState.alpha) { |
||||
return hasAlphaPanels; |
||||
} |
||||
return true; |
||||
} |
||||
Loading…
Reference in new issue