mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Panel Options: support dynamic options editors (#39491)
This commit is contained in:
parent
3a8d04603f
commit
3db98f417d
@ -2,6 +2,7 @@ import React from 'react';
|
|||||||
import { identityOverrideProcessor, standardEditorsRegistry, standardFieldConfigEditorRegistry } from '../field';
|
import { identityOverrideProcessor, standardEditorsRegistry, standardFieldConfigEditorRegistry } from '../field';
|
||||||
import { PanelPlugin } from './PanelPlugin';
|
import { PanelPlugin } from './PanelPlugin';
|
||||||
import { FieldConfigProperty } from '../types';
|
import { FieldConfigProperty } from '../types';
|
||||||
|
import { PanelOptionsEditorBuilder } from '..';
|
||||||
|
|
||||||
describe('PanelPlugin', () => {
|
describe('PanelPlugin', () => {
|
||||||
describe('declarative options', () => {
|
describe('declarative options', () => {
|
||||||
@ -70,8 +71,12 @@ describe('PanelPlugin', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(panel.optionEditors).toBeDefined();
|
const supplier = panel.getPanelOptionsSupplier();
|
||||||
expect(panel.optionEditors!.list()).toHaveLength(1);
|
expect(supplier).toBeDefined();
|
||||||
|
|
||||||
|
const builder = new PanelOptionsEditorBuilder();
|
||||||
|
supplier(builder, { data: [] });
|
||||||
|
expect(builder.getItems()).toHaveLength(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -3,7 +3,6 @@ import {
|
|||||||
GrafanaPlugin,
|
GrafanaPlugin,
|
||||||
PanelEditorProps,
|
PanelEditorProps,
|
||||||
PanelMigrationHandler,
|
PanelMigrationHandler,
|
||||||
PanelOptionEditorsRegistry,
|
|
||||||
PanelPluginMeta,
|
PanelPluginMeta,
|
||||||
PanelProps,
|
PanelProps,
|
||||||
PanelTypeChangedHandler,
|
PanelTypeChangedHandler,
|
||||||
@ -14,7 +13,7 @@ import { FieldConfigEditorBuilder, PanelOptionsEditorBuilder } from '../utils/Op
|
|||||||
import { ComponentClass, ComponentType } from 'react';
|
import { ComponentClass, ComponentType } from 'react';
|
||||||
import { set } from 'lodash';
|
import { set } from 'lodash';
|
||||||
import { deprecationWarning } from '../utils';
|
import { deprecationWarning } from '../utils';
|
||||||
import { FieldConfigOptionsRegistry } from '../field';
|
import { FieldConfigOptionsRegistry, StandardEditorContext } from '../field';
|
||||||
import { createFieldConfigRegistry } from './registryFactories';
|
import { createFieldConfigRegistry } from './registryFactories';
|
||||||
|
|
||||||
/** @beta */
|
/** @beta */
|
||||||
@ -84,6 +83,11 @@ export interface SetFieldConfigOptionsArgs<TFieldConfigOptions = any> {
|
|||||||
useCustomConfig?: (builder: FieldConfigEditorBuilder<TFieldConfigOptions>) => void;
|
useCustomConfig?: (builder: FieldConfigEditorBuilder<TFieldConfigOptions>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type PanelOptionsSupplier<TOptions> = (
|
||||||
|
builder: PanelOptionsEditorBuilder<TOptions>,
|
||||||
|
context: StandardEditorContext<TOptions>
|
||||||
|
) => void;
|
||||||
|
|
||||||
export class PanelPlugin<
|
export class PanelPlugin<
|
||||||
TOptions = any,
|
TOptions = any,
|
||||||
TFieldConfigOptions extends object = any
|
TFieldConfigOptions extends object = any
|
||||||
@ -99,8 +103,7 @@ export class PanelPlugin<
|
|||||||
return new FieldConfigOptionsRegistry();
|
return new FieldConfigOptionsRegistry();
|
||||||
};
|
};
|
||||||
|
|
||||||
private _optionEditors?: PanelOptionEditorsRegistry;
|
private optionsSupplier?: PanelOptionsSupplier<TOptions>;
|
||||||
private registerOptionEditors?: (builder: PanelOptionsEditorBuilder<TOptions>) => void;
|
|
||||||
|
|
||||||
panel: ComponentType<PanelProps<TOptions>> | null;
|
panel: ComponentType<PanelProps<TOptions>> | null;
|
||||||
editor?: ComponentClass<PanelEditorProps<TOptions>>;
|
editor?: ComponentClass<PanelEditorProps<TOptions>>;
|
||||||
@ -125,15 +128,13 @@ export class PanelPlugin<
|
|||||||
get defaults() {
|
get defaults() {
|
||||||
let result = this._defaults || {};
|
let result = this._defaults || {};
|
||||||
|
|
||||||
if (!this._defaults) {
|
if (!this._defaults && this.optionsSupplier) {
|
||||||
const editors = this.optionEditors;
|
const builder = new PanelOptionsEditorBuilder<TOptions>();
|
||||||
|
this.optionsSupplier(builder, { data: [] });
|
||||||
if (!editors || editors.list().length === 0) {
|
for (const item of builder.getItems()) {
|
||||||
return null;
|
if (item.defaultValue != null) {
|
||||||
|
set(result, item.path, item.defaultValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const editor of editors.list()) {
|
|
||||||
set(result, editor.id, editor.defaultValue);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -177,19 +178,6 @@ export class PanelPlugin<
|
|||||||
return this._fieldConfigRegistry;
|
return this._fieldConfigRegistry;
|
||||||
}
|
}
|
||||||
|
|
||||||
get optionEditors(): PanelOptionEditorsRegistry {
|
|
||||||
if (!this._optionEditors) {
|
|
||||||
const builder = new PanelOptionsEditorBuilder<TOptions>();
|
|
||||||
this._optionEditors = builder.getRegistry();
|
|
||||||
|
|
||||||
if (this.registerOptionEditors) {
|
|
||||||
this.registerOptionEditors(builder);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return this._optionEditors;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated setEditor is deprecated in favor of setPanelOptions
|
* @deprecated setEditor is deprecated in favor of setPanelOptions
|
||||||
*/
|
*/
|
||||||
@ -258,12 +246,21 @@ export class PanelPlugin<
|
|||||||
*
|
*
|
||||||
* @public
|
* @public
|
||||||
**/
|
**/
|
||||||
setPanelOptions(builder: (builder: PanelOptionsEditorBuilder<TOptions>) => void) {
|
setPanelOptions(builder: PanelOptionsSupplier<TOptions>) {
|
||||||
// builder is applied lazily when options UI is created
|
// builder is applied lazily when options UI is created
|
||||||
this.registerOptionEditors = builder;
|
this.optionsSupplier = builder;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is used while building the panel options editor.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
getPanelOptionsSupplier(): PanelOptionsSupplier<TOptions> {
|
||||||
|
return this.optionsSupplier ?? ((() => {}) as PanelOptionsSupplier<TOptions>);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tells Grafana if the plugin should subscribe to annotation and alertState results.
|
* Tells Grafana if the plugin should subscribe to annotation and alertState results.
|
||||||
*
|
*
|
||||||
|
@ -92,4 +92,8 @@ export abstract class OptionsUIRegistryBuilder<
|
|||||||
return this.properties;
|
return this.properties;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getItems() {
|
||||||
|
return this.properties;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { FieldConfigEditorProps, FieldConfigPropertyItem, FieldConfigEditorConfig } from '../types/fieldOverrides';
|
import { FieldConfigEditorProps, FieldConfigPropertyItem, FieldConfigEditorConfig } from '../types/fieldOverrides';
|
||||||
import { OptionsUIRegistryBuilder } from '../types/OptionsUIRegistryBuilder';
|
import { OptionsEditorItem, OptionsUIRegistryBuilder } from '../types/OptionsUIRegistryBuilder';
|
||||||
import { PanelOptionsEditorConfig, PanelOptionsEditorItem } from '../types/panel';
|
import { PanelOptionsEditorConfig, PanelOptionsEditorItem } from '../types/panel';
|
||||||
import {
|
import {
|
||||||
numberOverrideProcessor,
|
numberOverrideProcessor,
|
||||||
@ -17,6 +17,7 @@ import {
|
|||||||
unitOverrideProcessor,
|
unitOverrideProcessor,
|
||||||
FieldNamePickerConfigSettings,
|
FieldNamePickerConfigSettings,
|
||||||
} from '../field';
|
} from '../field';
|
||||||
|
import { PanelOptionsSupplier } from '../panel/PanelPlugin';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fluent API for declarative creation of field config option editors
|
* Fluent API for declarative creation of field config option editors
|
||||||
@ -129,6 +130,53 @@ export class FieldConfigEditorBuilder<TOptions> extends OptionsUIRegistryBuilder
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface NestedValueAccess {
|
||||||
|
getValue: (path: string) => any;
|
||||||
|
onChange: (path: string, value: any) => void;
|
||||||
|
}
|
||||||
|
export interface NestedPanelOptions<TSub = any> {
|
||||||
|
path: string;
|
||||||
|
category?: string[];
|
||||||
|
defaultValue?: TSub;
|
||||||
|
build: PanelOptionsSupplier<TSub>;
|
||||||
|
values?: (parent: NestedValueAccess) => NestedValueAccess;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NestedPanelOptionsBuilder<TSub = any> implements OptionsEditorItem<TSub, any, any, any> {
|
||||||
|
path = '';
|
||||||
|
category?: string[];
|
||||||
|
defaultValue?: TSub;
|
||||||
|
id = 'nested-panel-options';
|
||||||
|
name = 'nested';
|
||||||
|
editor = () => null;
|
||||||
|
|
||||||
|
constructor(public cfg: NestedPanelOptions<TSub>) {
|
||||||
|
this.path = cfg.path;
|
||||||
|
this.category = cfg.category;
|
||||||
|
this.defaultValue = cfg.defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
getBuilder = () => {
|
||||||
|
return this.cfg.build;
|
||||||
|
};
|
||||||
|
|
||||||
|
getNestedValueAccess = (parent: NestedValueAccess) => {
|
||||||
|
const values = this.cfg.values;
|
||||||
|
if (values) {
|
||||||
|
return values(parent);
|
||||||
|
}
|
||||||
|
// by default prefix the path
|
||||||
|
return {
|
||||||
|
getValue: (path: string) => parent.getValue(`${this.path}.${path}`),
|
||||||
|
onChange: (path: string, value: any) => parent.onChange(`${this.path}.${path}`, value),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isNestedPanelOptions(item: any): item is NestedPanelOptionsBuilder {
|
||||||
|
return item.id === 'nested-panel-options';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fluent API for declarative creation of panel options
|
* Fluent API for declarative creation of panel options
|
||||||
*/
|
*/
|
||||||
@ -137,6 +185,11 @@ export class PanelOptionsEditorBuilder<TOptions> extends OptionsUIRegistryBuilde
|
|||||||
StandardEditorProps,
|
StandardEditorProps,
|
||||||
PanelOptionsEditorItem<TOptions>
|
PanelOptionsEditorItem<TOptions>
|
||||||
> {
|
> {
|
||||||
|
addNestedOptions<Sub>(opts: NestedPanelOptions<Sub>) {
|
||||||
|
const s = new NestedPanelOptionsBuilder<Sub>(opts);
|
||||||
|
return this.addCustomEditor(s);
|
||||||
|
}
|
||||||
|
|
||||||
addNumberInput<TSettings>(config: PanelOptionsEditorConfig<TOptions, TSettings & NumberFieldConfigSettings, number>) {
|
addNumberInput<TSettings>(config: PanelOptionsEditorConfig<TOptions, TSettings & NumberFieldConfigSettings, number>) {
|
||||||
return this.addCustomEditor({
|
return this.addCustomEditor({
|
||||||
...config,
|
...config,
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { ComponentType } from 'react';
|
import { ComponentType } from 'react';
|
||||||
import { PanelOptionsEditorBuilder, RegistryItem } from '@grafana/data';
|
import { RegistryItem } from '@grafana/data';
|
||||||
import { Anchor, BackgroundConfig, LineConfig, Placement } from './types';
|
import { Anchor, BackgroundConfig, LineConfig, Placement } from './types';
|
||||||
import { DimensionContext } from '../dimensions/context';
|
import { DimensionContext } from '../dimensions/context';
|
||||||
|
import { PanelOptionsSupplier } from '@grafana/data/src/panel/PanelPlugin';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This gets saved in panel json
|
* This gets saved in panel json
|
||||||
@ -52,5 +53,5 @@ export interface CanvasElementItem<TConfig = any, TData = any> extends RegistryI
|
|||||||
display: ComponentType<CanvasElementProps<TConfig, TData>>;
|
display: ComponentType<CanvasElementProps<TConfig, TData>>;
|
||||||
|
|
||||||
/** Build the configuraiton UI */
|
/** Build the configuraiton UI */
|
||||||
registerOptionsUI?: (builder: PanelOptionsEditorBuilder<CanvasElementOptions<TConfig>>) => void;
|
registerOptionsUI?: PanelOptionsSupplier<CanvasElementOptions<TConfig>>;
|
||||||
}
|
}
|
||||||
|
@ -103,8 +103,10 @@ export const iconItem: CanvasElementItem<IconConfig, IconData> = {
|
|||||||
|
|
||||||
// Heatmap overlay options
|
// Heatmap overlay options
|
||||||
registerOptionsUI: (builder) => {
|
registerOptionsUI: (builder) => {
|
||||||
|
const category = ['Icon'];
|
||||||
builder
|
builder
|
||||||
.addCustomEditor({
|
.addCustomEditor({
|
||||||
|
category,
|
||||||
id: 'iconSelector',
|
id: 'iconSelector',
|
||||||
path: 'config.path',
|
path: 'config.path',
|
||||||
name: 'SVG Path',
|
name: 'SVG Path',
|
||||||
@ -114,9 +116,10 @@ export const iconItem: CanvasElementItem<IconConfig, IconData> = {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
.addCustomEditor({
|
.addCustomEditor({
|
||||||
|
category,
|
||||||
id: 'config.fill',
|
id: 'config.fill',
|
||||||
path: 'config.fill',
|
path: 'config.fill',
|
||||||
name: 'Icon fill color',
|
name: 'Fill color',
|
||||||
editor: ColorDimensionEditor,
|
editor: ColorDimensionEditor,
|
||||||
settings: {},
|
settings: {},
|
||||||
defaultValue: {
|
defaultValue: {
|
||||||
@ -125,6 +128,7 @@ export const iconItem: CanvasElementItem<IconConfig, IconData> = {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
.addSliderInput({
|
.addSliderInput({
|
||||||
|
category,
|
||||||
path: 'config.stroke.width',
|
path: 'config.stroke.width',
|
||||||
name: 'Stroke',
|
name: 'Stroke',
|
||||||
defaultValue: 0,
|
defaultValue: 0,
|
||||||
@ -134,16 +138,17 @@ export const iconItem: CanvasElementItem<IconConfig, IconData> = {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
.addCustomEditor({
|
.addCustomEditor({
|
||||||
|
category,
|
||||||
id: 'config.stroke.color',
|
id: 'config.stroke.color',
|
||||||
path: 'config.stroke.color',
|
path: 'config.stroke.color',
|
||||||
name: 'Icon Stroke color',
|
name: 'Stroke color',
|
||||||
editor: ColorDimensionEditor,
|
editor: ColorDimensionEditor,
|
||||||
settings: {},
|
settings: {},
|
||||||
defaultValue: {
|
defaultValue: {
|
||||||
// Configured values
|
// Configured values
|
||||||
fixed: 'grey',
|
fixed: 'grey',
|
||||||
},
|
},
|
||||||
showIf: (cfg) => Boolean(cfg.config?.stroke?.width),
|
showIf: (cfg) => Boolean(cfg?.config?.stroke?.width),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -97,14 +97,17 @@ export const textBoxItem: CanvasElementItem<TextBoxConfig, TextBoxData> = {
|
|||||||
|
|
||||||
// Heatmap overlay options
|
// Heatmap overlay options
|
||||||
registerOptionsUI: (builder) => {
|
registerOptionsUI: (builder) => {
|
||||||
|
const category = ['Text box'];
|
||||||
builder
|
builder
|
||||||
.addCustomEditor({
|
.addCustomEditor({
|
||||||
|
category,
|
||||||
id: 'textSelector',
|
id: 'textSelector',
|
||||||
path: 'config.text',
|
path: 'config.text',
|
||||||
name: 'Text',
|
name: 'Text',
|
||||||
editor: TextDimensionEditor,
|
editor: TextDimensionEditor,
|
||||||
})
|
})
|
||||||
.addCustomEditor({
|
.addCustomEditor({
|
||||||
|
category,
|
||||||
id: 'config.color',
|
id: 'config.color',
|
||||||
path: 'config.color',
|
path: 'config.color',
|
||||||
name: 'Text color',
|
name: 'Text color',
|
||||||
@ -113,6 +116,7 @@ export const textBoxItem: CanvasElementItem<TextBoxConfig, TextBoxData> = {
|
|||||||
defaultValue: {},
|
defaultValue: {},
|
||||||
})
|
})
|
||||||
.addRadio({
|
.addRadio({
|
||||||
|
category,
|
||||||
path: 'config.align',
|
path: 'config.align',
|
||||||
name: 'Align text',
|
name: 'Align text',
|
||||||
settings: {
|
settings: {
|
||||||
@ -125,6 +129,7 @@ export const textBoxItem: CanvasElementItem<TextBoxConfig, TextBoxData> = {
|
|||||||
defaultValue: Align.Left,
|
defaultValue: Align.Left,
|
||||||
})
|
})
|
||||||
.addRadio({
|
.addRadio({
|
||||||
|
category,
|
||||||
path: 'config.valign',
|
path: 'config.valign',
|
||||||
name: 'Vertical align',
|
name: 'Vertical align',
|
||||||
settings: {
|
settings: {
|
||||||
@ -137,6 +142,7 @@ export const textBoxItem: CanvasElementItem<TextBoxConfig, TextBoxData> = {
|
|||||||
defaultValue: VAlign.Middle,
|
defaultValue: VAlign.Middle,
|
||||||
})
|
})
|
||||||
.addNumberInput({
|
.addNumberInput({
|
||||||
|
category,
|
||||||
path: 'config.size',
|
path: 'config.size',
|
||||||
name: 'Text size',
|
name: 'Text size',
|
||||||
settings: {
|
settings: {
|
||||||
|
@ -37,6 +37,19 @@ export class OptionsPaneCategoryDescriptor {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getCategory(name: string): OptionsPaneCategoryDescriptor {
|
||||||
|
let sub = this.categories.find((c) => c.props.id === name);
|
||||||
|
if (sub) {
|
||||||
|
return sub;
|
||||||
|
}
|
||||||
|
sub = new OptionsPaneCategoryDescriptor({
|
||||||
|
title: name,
|
||||||
|
id: name,
|
||||||
|
});
|
||||||
|
this.addCategory(sub);
|
||||||
|
return sub;
|
||||||
|
}
|
||||||
|
|
||||||
render(searchQuery?: string) {
|
render(searchQuery?: string) {
|
||||||
if (this.props.customRender) {
|
if (this.props.customRender) {
|
||||||
return this.props.customRender();
|
return this.props.customRender();
|
||||||
|
@ -1,11 +1,17 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { PanelOptionsEditorItem, StandardEditorContext, VariableSuggestionsScope } from '@grafana/data';
|
import { StandardEditorContext, VariableSuggestionsScope } from '@grafana/data';
|
||||||
import { get as lodashGet } from 'lodash';
|
import { get as lodashGet } from 'lodash';
|
||||||
import { getDataLinksVariableSuggestions } from 'app/features/panel/panellinks/link_srv';
|
import { getDataLinksVariableSuggestions } from 'app/features/panel/panellinks/link_srv';
|
||||||
import { OptionPaneRenderProps } from './types';
|
import { OptionPaneRenderProps } from './types';
|
||||||
import { updateDefaultFieldConfigValue, setOptionImmutably } from './utils';
|
import { updateDefaultFieldConfigValue, setOptionImmutably } from './utils';
|
||||||
import { OptionsPaneItemDescriptor } from './OptionsPaneItemDescriptor';
|
import { OptionsPaneItemDescriptor } from './OptionsPaneItemDescriptor';
|
||||||
import { OptionsPaneCategoryDescriptor } from './OptionsPaneCategoryDescriptor';
|
import { OptionsPaneCategoryDescriptor } from './OptionsPaneCategoryDescriptor';
|
||||||
|
import {
|
||||||
|
isNestedPanelOptions,
|
||||||
|
NestedValueAccess,
|
||||||
|
PanelOptionsEditorBuilder,
|
||||||
|
} from '../../../../../../packages/grafana-data/src/utils/OptionsUIBuilders';
|
||||||
|
import { PanelOptionsSupplier } from '@grafana/data/src/panel/PanelPlugin';
|
||||||
|
|
||||||
type categoryGetter = (categoryNames?: string[]) => OptionsPaneCategoryDescriptor;
|
type categoryGetter = (categoryNames?: string[]) => OptionsPaneCategoryDescriptor;
|
||||||
|
|
||||||
@ -40,16 +46,16 @@ export function getVizualizationOptions(props: OptionPaneRenderProps): OptionsPa
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
// Load the options into categories
|
const access: NestedValueAccess = {
|
||||||
fillOptionsPaneItems(
|
getValue: (path: string) => lodashGet(currentOptions, path),
|
||||||
plugin.optionEditors.list(),
|
onChange: (path: string, value: any) => {
|
||||||
getOptionsPaneCategory,
|
const newOptions = setOptionImmutably(currentOptions, path, value);
|
||||||
(path: string, value: any) => {
|
|
||||||
const newOptions = setOptionImmutably(context.options, path, value);
|
|
||||||
onPanelOptionsChanged(newOptions);
|
onPanelOptionsChanged(newOptions);
|
||||||
},
|
},
|
||||||
context
|
};
|
||||||
);
|
|
||||||
|
// Load the options into categories
|
||||||
|
fillOptionsPaneItems(plugin.getPanelOptionsSupplier(), access, getOptionsPaneCategory, context);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Field options
|
* Field options
|
||||||
@ -107,21 +113,41 @@ export function getVizualizationOptions(props: OptionPaneRenderProps): OptionsPa
|
|||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
export function fillOptionsPaneItems(
|
export function fillOptionsPaneItems(
|
||||||
optionEditors: PanelOptionsEditorItem[],
|
supplier: PanelOptionsSupplier<any>,
|
||||||
|
access: NestedValueAccess,
|
||||||
getOptionsPaneCategory: categoryGetter,
|
getOptionsPaneCategory: categoryGetter,
|
||||||
onValueChanged: (path: string, value: any) => void,
|
context: StandardEditorContext<any, any>,
|
||||||
context: StandardEditorContext<any, any>
|
parentCategory?: OptionsPaneCategoryDescriptor
|
||||||
) {
|
) {
|
||||||
for (const pluginOption of optionEditors) {
|
const builder = new PanelOptionsEditorBuilder<any>();
|
||||||
|
supplier(builder, context);
|
||||||
|
|
||||||
|
for (const pluginOption of builder.getItems()) {
|
||||||
if (pluginOption.showIf && !pluginOption.showIf(context.options, context.data)) {
|
if (pluginOption.showIf && !pluginOption.showIf(context.options, context.data)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const category = getOptionsPaneCategory(pluginOption.category);
|
let category = parentCategory;
|
||||||
|
if (!category) {
|
||||||
|
category = getOptionsPaneCategory(pluginOption.category);
|
||||||
|
} else if (pluginOption.category?.[0]?.length) {
|
||||||
|
category = category.getCategory(pluginOption.category[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nested options get passed up one level
|
||||||
|
if (isNestedPanelOptions(pluginOption)) {
|
||||||
|
const sub = access.getValue(pluginOption.path);
|
||||||
|
fillOptionsPaneItems(
|
||||||
|
pluginOption.getBuilder(),
|
||||||
|
pluginOption.getNestedValueAccess(access),
|
||||||
|
getOptionsPaneCategory,
|
||||||
|
{ ...context, options: sub },
|
||||||
|
category // parent category
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const Editor = pluginOption.editor;
|
const Editor = pluginOption.editor;
|
||||||
|
|
||||||
// TODO? can some options recursivly call: fillOptionsPaneItems?
|
|
||||||
|
|
||||||
category.addItem(
|
category.addItem(
|
||||||
new OptionsPaneItemDescriptor({
|
new OptionsPaneItemDescriptor({
|
||||||
title: pluginOption.name,
|
title: pluginOption.name,
|
||||||
@ -129,9 +155,9 @@ export function fillOptionsPaneItems(
|
|||||||
render: function renderEditor() {
|
render: function renderEditor() {
|
||||||
return (
|
return (
|
||||||
<Editor
|
<Editor
|
||||||
value={lodashGet(context.options, pluginOption.path)}
|
value={access.getValue(pluginOption.path)}
|
||||||
onChange={(value: any) => {
|
onChange={(value: any) => {
|
||||||
onValueChanged(pluginOption.path, value);
|
access.onChange(pluginOption.path, value);
|
||||||
}}
|
}}
|
||||||
item={pluginOption}
|
item={pluginOption}
|
||||||
context={context}
|
context={context}
|
||||||
|
@ -121,7 +121,7 @@ export function ResourcePicker(props: Props) {
|
|||||||
<TabContent>
|
<TabContent>
|
||||||
{tabs[0].active && (
|
{tabs[0].active && (
|
||||||
<div className={styles.tabContent}>
|
<div className={styles.tabContent}>
|
||||||
<Select options={folders} onChange={setCurrentFolder} value={currentFolder} />
|
<Select menuShouldPortal={true} options={folders} onChange={setCurrentFolder} value={currentFolder} />
|
||||||
<Input placeholder="Search" onChange={onChangeSearch} />
|
<Input placeholder="Search" onChange={onChangeSearch} />
|
||||||
{filteredIndex ? (
|
{filteredIndex ? (
|
||||||
<div className={styles.cardsWrapper}>
|
<div className={styles.cardsWrapper}>
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
import { Component } from 'react';
|
import { Component } from 'react';
|
||||||
import { PanelProps } from '@grafana/data';
|
import { CoreApp, PanelProps } from '@grafana/data';
|
||||||
import { PanelOptions } from './models.gen';
|
import { PanelOptions } from './models.gen';
|
||||||
import { ReplaySubject, Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
import { PanelEditExitedEvent } from 'app/types/events';
|
import { PanelEditExitedEvent } from 'app/types/events';
|
||||||
import { CanvasGroupOptions } from 'app/features/canvas';
|
import { CanvasGroupOptions } from 'app/features/canvas';
|
||||||
import { Scene } from 'app/features/canvas/runtime/scene';
|
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> {}
|
interface Props extends PanelProps<PanelOptions> {}
|
||||||
|
|
||||||
@ -12,10 +14,15 @@ interface State {
|
|||||||
refresh: number;
|
refresh: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Used to pass the scene to the editor functions
|
export interface InstanceState {
|
||||||
export const theScene = new ReplaySubject<Scene>(1);
|
scene: Scene;
|
||||||
|
selected?: ElementState;
|
||||||
|
}
|
||||||
|
|
||||||
export class CanvasPanel extends Component<Props, State> {
|
export class CanvasPanel extends Component<Props, State> {
|
||||||
|
static contextType = PanelContextRoot;
|
||||||
|
panelContext: PanelContext = {} as PanelContext;
|
||||||
|
|
||||||
readonly scene: Scene;
|
readonly scene: Scene;
|
||||||
private subs = new Subscription();
|
private subs = new Subscription();
|
||||||
needsReload = false;
|
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 = new Scene(this.props.options.root, this.onUpdateScene);
|
||||||
this.scene.updateSize(props.width, props.height);
|
this.scene.updateSize(props.width, props.height);
|
||||||
this.scene.updateData(props.data);
|
this.scene.updateData(props.data);
|
||||||
theScene.next(this.scene); // used in the editors
|
|
||||||
|
|
||||||
this.subs.add(
|
this.subs.add(
|
||||||
this.props.eventBus.subscribe(PanelEditExitedEvent, (evt) => {
|
this.props.eventBus.subscribe(PanelEditExitedEvent, (evt) => {
|
||||||
@ -43,7 +49,23 @@ export class CanvasPanel extends Component<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
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() {
|
componentWillUnmount() {
|
||||||
|
@ -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);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
75
public/app/plugins/panel/canvas/editor/elementEditor.tsx
Normal file
75
public/app/plugins/panel/canvas/editor/elementEditor.tsx
Normal 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);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
@ -1,13 +1,21 @@
|
|||||||
import { PanelOptionsEditorBuilder } from '@grafana/data';
|
import { PanelOptionsSupplier } from '@grafana/data/src/panel/PanelPlugin';
|
||||||
import { BackgroundImageSize } from 'app/features/canvas';
|
import { BackgroundImageSize, CanvasElementOptions } from 'app/features/canvas';
|
||||||
import { ColorDimensionEditor, ResourceDimensionEditor } from 'app/features/dimensions/editors';
|
import { ColorDimensionEditor, ResourceDimensionEditor } from 'app/features/dimensions/editors';
|
||||||
|
|
||||||
export function addBackgroundOptions(builder: PanelOptionsEditorBuilder<any>) {
|
interface OptionSuppliers {
|
||||||
|
addBackground: PanelOptionsSupplier<CanvasElementOptions>;
|
||||||
|
addBorder: PanelOptionsSupplier<CanvasElementOptions>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const optionBuilder: OptionSuppliers = {
|
||||||
|
addBackground: (builder, context) => {
|
||||||
|
const category = ['Background'];
|
||||||
builder
|
builder
|
||||||
.addCustomEditor({
|
.addCustomEditor({
|
||||||
|
category,
|
||||||
id: 'background.color',
|
id: 'background.color',
|
||||||
path: 'background.color',
|
path: 'background.color',
|
||||||
name: 'Background Color',
|
name: 'Color',
|
||||||
editor: ColorDimensionEditor,
|
editor: ColorDimensionEditor,
|
||||||
settings: {},
|
settings: {},
|
||||||
defaultValue: {
|
defaultValue: {
|
||||||
@ -16,17 +24,19 @@ export function addBackgroundOptions(builder: PanelOptionsEditorBuilder<any>) {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
.addCustomEditor({
|
.addCustomEditor({
|
||||||
|
category,
|
||||||
id: 'background.image',
|
id: 'background.image',
|
||||||
path: 'background.image',
|
path: 'background.image',
|
||||||
name: 'Background Image',
|
name: 'Image',
|
||||||
editor: ResourceDimensionEditor,
|
editor: ResourceDimensionEditor,
|
||||||
settings: {
|
settings: {
|
||||||
resourceType: 'image',
|
resourceType: 'image',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.addRadio({
|
.addRadio({
|
||||||
|
category,
|
||||||
path: 'background.size',
|
path: 'background.size',
|
||||||
name: 'Backround image size',
|
name: 'Image size',
|
||||||
settings: {
|
settings: {
|
||||||
options: [
|
options: [
|
||||||
{ value: BackgroundImageSize.Original, label: 'Original' },
|
{ value: BackgroundImageSize.Original, label: 'Original' },
|
||||||
@ -38,12 +48,14 @@ export function addBackgroundOptions(builder: PanelOptionsEditorBuilder<any>) {
|
|||||||
},
|
},
|
||||||
defaultValue: BackgroundImageSize.Cover,
|
defaultValue: BackgroundImageSize.Cover,
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
|
||||||
export function addBorderOptions(builder: PanelOptionsEditorBuilder<any>) {
|
addBorder: (builder, context) => {
|
||||||
|
const category = ['Border'];
|
||||||
builder.addSliderInput({
|
builder.addSliderInput({
|
||||||
|
category,
|
||||||
path: 'border.width',
|
path: 'border.width',
|
||||||
name: 'Border Width',
|
name: 'Width',
|
||||||
defaultValue: 2,
|
defaultValue: 2,
|
||||||
settings: {
|
settings: {
|
||||||
min: 0,
|
min: 0,
|
||||||
@ -51,16 +63,19 @@ export function addBorderOptions(builder: PanelOptionsEditorBuilder<any>) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (context.options?.border?.width) {
|
||||||
builder.addCustomEditor({
|
builder.addCustomEditor({
|
||||||
|
category,
|
||||||
id: 'border.color',
|
id: 'border.color',
|
||||||
path: 'border.color',
|
path: 'border.color',
|
||||||
name: 'Border Color',
|
name: 'Color',
|
||||||
editor: ColorDimensionEditor,
|
editor: ColorDimensionEditor,
|
||||||
settings: {},
|
settings: {},
|
||||||
defaultValue: {
|
defaultValue: {
|
||||||
// Configured values
|
// Configured values
|
||||||
fixed: '',
|
fixed: '',
|
||||||
},
|
},
|
||||||
showIf: (cfg) => Boolean(cfg.border?.width),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
@ -8,10 +8,12 @@ import { CanvasGroupOptions, DEFAULT_CANVAS_ELEMENT_CONFIG } from 'app/features/
|
|||||||
export const modelVersion = Object.freeze([1, 0]);
|
export const modelVersion = Object.freeze([1, 0]);
|
||||||
|
|
||||||
export interface PanelOptions {
|
export interface PanelOptions {
|
||||||
|
inlineEditing: boolean;
|
||||||
root: CanvasGroupOptions;
|
root: CanvasGroupOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultPanelOptions: PanelOptions = {
|
export const defaultPanelOptions: PanelOptions = {
|
||||||
|
inlineEditing: true,
|
||||||
root: ({
|
root: ({
|
||||||
elements: [
|
elements: [
|
||||||
{
|
{
|
||||||
|
@ -1,19 +1,29 @@
|
|||||||
import { PanelPlugin } from '@grafana/data';
|
import { PanelPlugin } from '@grafana/data';
|
||||||
|
|
||||||
import { CanvasPanel } from './CanvasPanel';
|
import { CanvasPanel, InstanceState } from './CanvasPanel';
|
||||||
import { SelectedElementEditor } from './editor/SelectedElementEditor';
|
import { PanelOptions } from './models.gen';
|
||||||
import { defaultPanelOptions, PanelOptions } from './models.gen';
|
import { getElementEditor } from './editor/elementEditor';
|
||||||
|
|
||||||
export const plugin = new PanelPlugin<PanelOptions>(CanvasPanel)
|
export const plugin = new PanelPlugin<PanelOptions>(CanvasPanel)
|
||||||
.setNoPadding() // extend to panel edges
|
.setNoPadding() // extend to panel edges
|
||||||
.useFieldConfig()
|
.useFieldConfig()
|
||||||
.setPanelOptions((builder) => {
|
.setPanelOptions((builder, context) => {
|
||||||
builder.addCustomEditor({
|
const state: InstanceState = context.instanceState;
|
||||||
category: ['Selected Element'],
|
|
||||||
id: 'root',
|
builder.addBooleanSwitch({
|
||||||
path: 'root', // multiple elements may edit root!
|
path: 'inlineEditing',
|
||||||
name: 'Selected Element',
|
name: 'Inline editing',
|
||||||
editor: SelectedElementEditor,
|
description: 'Enable editing while the panel is in dashboard mode',
|
||||||
defaultValue: defaultPanelOptions.root,
|
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>
|
|
||||||
);
|
|
||||||
};
|
|
155
public/app/plugins/panel/geomap/editor/layerEditor.tsx
Normal file
155
public/app/plugins/panel/geomap/editor/layerEditor.tsx
Normal 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;
|
||||||
|
}
|
@ -1,18 +1,17 @@
|
|||||||
|
import React from 'react';
|
||||||
import { PanelPlugin } from '@grafana/data';
|
import { PanelPlugin } from '@grafana/data';
|
||||||
import { BaseLayerEditor } from './editor/BaseLayerEditor';
|
|
||||||
import { DataLayersEditor } from './editor/DataLayersEditor';
|
|
||||||
import { GeomapPanel } from './GeomapPanel';
|
import { GeomapPanel } from './GeomapPanel';
|
||||||
import { MapViewEditor } from './editor/MapViewEditor';
|
import { MapViewEditor } from './editor/MapViewEditor';
|
||||||
import { defaultView, GeomapPanelOptions } from './types';
|
import { defaultView, GeomapPanelOptions } from './types';
|
||||||
import { mapPanelChangedHandler } from './migrations';
|
import { mapPanelChangedHandler } from './migrations';
|
||||||
import { defaultMarkersConfig } from './layers/data/markersLayer';
|
import { getLayerEditor } from './editor/layerEditor';
|
||||||
import { DEFAULT_BASEMAP_CONFIG } from './layers/registry';
|
import { config } from '@grafana/runtime';
|
||||||
|
|
||||||
export const plugin = new PanelPlugin<GeomapPanelOptions>(GeomapPanel)
|
export const plugin = new PanelPlugin<GeomapPanelOptions>(GeomapPanel)
|
||||||
.setNoPadding()
|
.setNoPadding()
|
||||||
.setPanelChangeHandler(mapPanelChangedHandler)
|
.setPanelChangeHandler(mapPanelChangedHandler)
|
||||||
.useFieldConfig()
|
.useFieldConfig()
|
||||||
.setPanelOptions((builder) => {
|
.setPanelOptions((builder, context) => {
|
||||||
let category = ['Map view'];
|
let category = ['Map view'];
|
||||||
builder.addCustomEditor({
|
builder.addCustomEditor({
|
||||||
category,
|
category,
|
||||||
@ -32,23 +31,35 @@ export const plugin = new PanelPlugin<GeomapPanelOptions>(GeomapPanel)
|
|||||||
defaultValue: defaultView.shared,
|
defaultValue: defaultView.shared,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Check server settings to disable custom basemap settings
|
||||||
|
if (config.geomapDisableCustomBaseLayer) {
|
||||||
builder.addCustomEditor({
|
builder.addCustomEditor({
|
||||||
category: ['Base layer'],
|
category: ['Base layer'],
|
||||||
id: 'basemap',
|
|
||||||
path: 'basemap',
|
|
||||||
name: 'Base layer',
|
|
||||||
editor: BaseLayerEditor,
|
|
||||||
defaultValue: DEFAULT_BASEMAP_CONFIG,
|
|
||||||
});
|
|
||||||
|
|
||||||
builder.addCustomEditor({
|
|
||||||
category: ['Data layer'],
|
|
||||||
id: 'layers',
|
id: 'layers',
|
||||||
path: 'layers',
|
path: '',
|
||||||
name: 'Data layer',
|
name: '',
|
||||||
editor: DataLayersEditor,
|
// eslint-disable-next-line react/display-name
|
||||||
defaultValue: [defaultMarkersConfig],
|
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.addNestedOptions(
|
||||||
|
getLayerEditor({
|
||||||
|
category: ['Data layer'],
|
||||||
|
path: 'layers[0]', // only one for now
|
||||||
|
basemaps: false,
|
||||||
|
current: context.options?.layers?.[0],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
// The controls section
|
// The controls section
|
||||||
category = ['Map controls'];
|
category = ['Map controls'];
|
||||||
|
Loading…
Reference in New Issue
Block a user