Panel Options: support dynamic options editors (#39491)

This commit is contained in:
Ryan McKinley
2021-10-06 12:41:42 -07:00
committed by GitHub
parent 3a8d04603f
commit 3db98f417d
22 changed files with 554 additions and 553 deletions

View File

@@ -1,10 +1,12 @@
import { Component } from 'react';
import { PanelProps } from '@grafana/data';
import { CoreApp, PanelProps } from '@grafana/data';
import { PanelOptions } from './models.gen';
import { ReplaySubject, Subscription } from 'rxjs';
import { Subscription } from 'rxjs';
import { PanelEditExitedEvent } from 'app/types/events';
import { CanvasGroupOptions } from 'app/features/canvas';
import { Scene } from 'app/features/canvas/runtime/scene';
import { PanelContext, PanelContextRoot } from '@grafana/ui';
import { ElementState } from 'app/features/canvas/runtime/element';
interface Props extends PanelProps<PanelOptions> {}
@@ -12,10 +14,15 @@ interface State {
refresh: number;
}
// Used to pass the scene to the editor functions
export const theScene = new ReplaySubject<Scene>(1);
export interface InstanceState {
scene: Scene;
selected?: ElementState;
}
export class CanvasPanel extends Component<Props, State> {
static contextType = PanelContextRoot;
panelContext: PanelContext = {} as PanelContext;
readonly scene: Scene;
private subs = new Subscription();
needsReload = false;
@@ -31,7 +38,6 @@ export class CanvasPanel extends Component<Props, State> {
this.scene = new Scene(this.props.options.root, this.onUpdateScene);
this.scene.updateSize(props.width, props.height);
this.scene.updateData(props.data);
theScene.next(this.scene); // used in the editors
this.subs.add(
this.props.eventBus.subscribe(PanelEditExitedEvent, (evt) => {
@@ -43,7 +49,23 @@ export class CanvasPanel extends Component<Props, State> {
}
componentDidMount() {
theScene.next(this.scene);
this.panelContext = this.context as PanelContext;
if (this.panelContext.onInstanceStateChange && this.panelContext.app === CoreApp.PanelEditor) {
this.panelContext.onInstanceStateChange({
scene: this.scene,
});
this.subs.add(
this.scene.selected.subscribe({
next: (v) => {
this.panelContext.onInstanceStateChange!({
scene: this.scene,
selected: v,
});
},
})
);
}
}
componentWillUnmount() {

View File

@@ -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>
);
};

View File

@@ -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);
}}
/>
);
};

View File

@@ -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);
},
};
}

View File

@@ -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,
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: {
resourceType: 'image',
min: 0,
max: 20,
},
})
.addRadio({
path: 'background.size',
name: 'Backround 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,
});
}
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: '',
},
});
}
},
};

View File

@@ -8,10 +8,12 @@ import { CanvasGroupOptions, DEFAULT_CANVAS_ELEMENT_CONFIG } from 'app/features/
export const modelVersion = Object.freeze([1, 0]);
export interface PanelOptions {
inlineEditing: boolean;
root: CanvasGroupOptions;
}
export const defaultPanelOptions: PanelOptions = {
inlineEditing: true,
root: ({
elements: [
{

View File

@@ -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,
})
);
}
});

View File

@@ -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} />;
};

View File

@@ -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}
/>
);
};

View File

@@ -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>
);
};

View File

@@ -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;
}

View File

@@ -1,18 +1,17 @@
import React from 'react';
import { PanelPlugin } from '@grafana/data';
import { BaseLayerEditor } from './editor/BaseLayerEditor';
import { DataLayersEditor } from './editor/DataLayersEditor';
import { GeomapPanel } from './GeomapPanel';
import { MapViewEditor } from './editor/MapViewEditor';
import { defaultView, GeomapPanelOptions } from './types';
import { mapPanelChangedHandler } from './migrations';
import { defaultMarkersConfig } from './layers/data/markersLayer';
import { DEFAULT_BASEMAP_CONFIG } from './layers/registry';
import { getLayerEditor } from './editor/layerEditor';
import { config } from '@grafana/runtime';
export const plugin = new PanelPlugin<GeomapPanelOptions>(GeomapPanel)
.setNoPadding()
.setPanelChangeHandler(mapPanelChangedHandler)
.useFieldConfig()
.setPanelOptions((builder) => {
.setPanelOptions((builder, context) => {
let category = ['Map view'];
builder.addCustomEditor({
category,
@@ -32,23 +31,35 @@ export const plugin = new PanelPlugin<GeomapPanelOptions>(GeomapPanel)
defaultValue: defaultView.shared,
});
builder.addCustomEditor({
category: ['Base layer'],
id: 'basemap',
path: 'basemap',
name: 'Base layer',
editor: BaseLayerEditor,
defaultValue: DEFAULT_BASEMAP_CONFIG,
});
// Check server settings to disable custom basemap settings
if (config.geomapDisableCustomBaseLayer) {
builder.addCustomEditor({
category: ['Base layer'],
id: 'layers',
path: '',
name: '',
// eslint-disable-next-line react/display-name
editor: () => <div>The base layer is configured by the server admin.</div>,
});
} else {
builder.addNestedOptions(
getLayerEditor({
category: ['Base layer'],
path: 'basemap', // only one for now
basemaps: true,
current: context.options?.layers?.[0],
})
);
}
builder.addCustomEditor({
category: ['Data layer'],
id: 'layers',
path: 'layers',
name: 'Data layer',
editor: DataLayersEditor,
defaultValue: [defaultMarkersConfig],
});
builder.addNestedOptions(
getLayerEditor({
category: ['Data layer'],
path: 'layers[0]', // only one for now
basemaps: false,
current: context.options?.layers?.[0],
})
);
// The controls section
category = ['Map controls'];