diff --git a/public/app/features/dimensions/editors/ResourceDimensionEditor.tsx b/public/app/features/dimensions/editors/ResourceDimensionEditor.tsx
index b85eadb6323..edd5de0cb1e 100644
--- a/public/app/features/dimensions/editors/ResourceDimensionEditor.tsx
+++ b/public/app/features/dimensions/editors/ResourceDimensionEditor.tsx
@@ -63,6 +63,11 @@ export const ResourceDimensionEditor: FC<
[onChange, value]
);
+ const onClear = (event: React.MouseEvent) => {
+ event.stopPropagation();
+ onChange({ mode: ResourceDimensionMode.Fixed, fixed: '', field: '' });
+ };
+
const openModal = useCallback(() => {
setOpen(true);
}, []);
@@ -71,7 +76,14 @@ export const ResourceDimensionEditor: FC<
const showSourceRadio = item.settings?.showSourceRadio ?? true;
const mediaType = item.settings?.resourceType ?? 'icon';
const folderName = item.settings?.folderName ?? ResourceFolderName.Icon;
- const srcPath = mediaType === 'icon' && value ? getPublicOrAbsoluteUrl(value?.fixed) : '';
+ let srcPath = '';
+ if (mediaType === 'icon') {
+ if (value?.fixed) {
+ srcPath = getPublicOrAbsoluteUrl(value.fixed);
+ } else if (item.settings?.placeholderValue) {
+ srcPath = getPublicOrAbsoluteUrl(item.settings.placeholderValue);
+ }
+ }
return (
<>
@@ -106,15 +118,14 @@ export const ResourceDimensionEditor: FC<
)}
{mode === ResourceDimensionMode.Fixed && (
-
+
}
- suffix={}
+ suffix={}
/>
@@ -148,4 +159,10 @@ const getStyles = (theme: GrafanaTheme2) => ({
fill: currentColor;
max-width: 25px;
`,
+ pointer: css`
+ cursor: pointer;
+ input[readonly] {
+ cursor: pointer;
+ }
+ `,
});
diff --git a/public/app/features/dimensions/editors/ResourcePicker.tsx b/public/app/features/dimensions/editors/ResourcePicker.tsx
index 35ecf7a0880..59e87032eeb 100644
--- a/public/app/features/dimensions/editors/ResourcePicker.tsx
+++ b/public/app/features/dimensions/editors/ResourcePicker.tsx
@@ -39,8 +39,8 @@ const getFolders = (mediaType: 'icon' | 'image') => {
}
};
-const folderIfExists = (folders: Array<{ label: string; value: string }>, path: string) => {
- return folders.filter((folder) => path.indexOf(folder.value) > -1)[0] ?? folders[0];
+const getFolderIfExists = (folders: Array>, path: string) => {
+ return folders.find((folder) => path.startsWith(folder.value!)) ?? folders[0];
};
export const ResourcePicker = (props: Props) => {
@@ -50,8 +50,9 @@ export const ResourcePicker = (props: Props) => {
value: v,
}));
- const folderOfCurrentValue = value || folderName ? folderIfExists(folders, value ?? folderName) : folders[0];
- const [currentFolder, setCurrentFolder] = useState>(folderOfCurrentValue);
+ const [currentFolder, setCurrentFolder] = useState>(
+ getFolderIfExists(folders, value?.length ? value : folderName)
+ );
const [directoryIndex, setDirectoryIndex] = useState([]);
const [filteredIndex, setFilteredIndex] = useState([]);
// select between existing icon folder, url, or upload
diff --git a/public/app/features/dimensions/editors/TextDimensionEditor.tsx b/public/app/features/dimensions/editors/TextDimensionEditor.tsx
index 275a2521536..554e42895c9 100644
--- a/public/app/features/dimensions/editors/TextDimensionEditor.tsx
+++ b/public/app/features/dimensions/editors/TextDimensionEditor.tsx
@@ -5,8 +5,9 @@ import {
StandardEditorsRegistryItem,
StringFieldConfigSettings,
} from '@grafana/data';
+import { Button, InlineField, InlineFieldRow, RadioButtonGroup, StringValueEditor } from '@grafana/ui';
+
import { TextDimensionConfig, TextDimensionMode, TextDimensionOptions } from '../types';
-import { InlineField, InlineFieldRow, RadioButtonGroup, StringValueEditor } from '@grafana/ui';
import { FieldNamePicker } from '../../../../../packages/grafana-ui/src/components/MatchersUI/FieldNamePicker';
const textOptions = [
@@ -57,6 +58,12 @@ export const TextDimensionEditor: FC {
+ // Need to first change to field in order to clear fixed value in editor
+ onChange({ mode: TextDimensionMode.Field, fixed: '', field: '' });
+ onChange({ mode: TextDimensionMode.Fixed, fixed: '', field: '' });
+ };
+
const mode = value?.mode ?? TextDimensionMode.Fixed;
return (
@@ -81,12 +88,17 @@ export const TextDimensionEditor: FC
-
+ <>
+
+ {value.fixed && (
+
+ )}
+ >
)}
diff --git a/public/app/features/dimensions/types.ts b/public/app/features/dimensions/types.ts
index 097e244cb46..4edf91a0731 100644
--- a/public/app/features/dimensions/types.ts
+++ b/public/app/features/dimensions/types.ts
@@ -65,6 +65,12 @@ export interface TextDimensionConfig extends BaseDimensionConfig {
mode: TextDimensionMode;
}
+export const defaultTextConfig: TextDimensionConfig = Object.freeze({
+ fixed: '',
+ mode: TextDimensionMode.Field,
+ field: '',
+});
+
/** Use the color value from field configs */
export interface ColorDimensionConfig extends BaseDimensionConfig {}
@@ -72,6 +78,9 @@ export interface ColorDimensionConfig extends BaseDimensionConfig {}
export interface ResourceDimensionOptions {
resourceType: 'icon' | 'image';
folderName?: ResourceFolderName;
+ placeholderText?: string;
+ placeholderValue?: string;
+ // If you want your icon to be driven by value of a field
showSourceRadio?: boolean;
}
diff --git a/public/app/plugins/panel/geomap/layers/data/StyleEditor.tsx b/public/app/plugins/panel/geomap/layers/data/StyleEditor.tsx
new file mode 100644
index 00000000000..899be740bb4
--- /dev/null
+++ b/public/app/plugins/panel/geomap/layers/data/StyleEditor.tsx
@@ -0,0 +1,185 @@
+import React, { FC } from 'react';
+import { StandardEditorProps } from '@grafana/data';
+import { Field, HorizontalGroup, NumberValueEditor, RadioButtonGroup, SliderValueEditor } from '@grafana/ui';
+
+import {
+ ColorDimensionEditor,
+ ResourceDimensionEditor,
+ ScaleDimensionEditor,
+ TextDimensionEditor,
+} from 'app/features/dimensions/editors';
+import {
+ ScaleDimensionConfig,
+ ResourceDimensionConfig,
+ ColorDimensionConfig,
+ ResourceFolderName,
+ TextDimensionConfig,
+ defaultTextConfig,
+} from 'app/features/dimensions/types';
+import { defaultStyleConfig, StyleConfig, TextAlignment, TextBaseline } from '../../style/types';
+
+export const StyleEditor: FC> = ({ value, context, onChange }) => {
+ const onSizeChange = (sizeValue: ScaleDimensionConfig | undefined) => {
+ onChange({ ...value, size: sizeValue });
+ };
+
+ const onSymbolChange = (symbolValue: ResourceDimensionConfig | undefined) => {
+ onChange({ ...value, symbol: symbolValue });
+ };
+
+ const onColorChange = (colorValue: ColorDimensionConfig | undefined) => {
+ onChange({ ...value, color: colorValue });
+ };
+
+ const onOpacityChange = (opacityValue: number | undefined) => {
+ onChange({ ...value, opacity: opacityValue });
+ };
+
+ const onTextChange = (textValue: TextDimensionConfig | undefined) => {
+ onChange({ ...value, text: textValue });
+ };
+
+ const onTextFontSizeChange = (fontSize: number | undefined) => {
+ onChange({ ...value, textConfig: { ...value.textConfig, fontSize } });
+ };
+
+ const onTextOffsetXChange = (offsetX: number | undefined) => {
+ onChange({ ...value, textConfig: { ...value.textConfig, offsetX } });
+ };
+
+ const onTextOffsetYChange = (offsetY: number | undefined) => {
+ onChange({ ...value, textConfig: { ...value.textConfig, offsetY } });
+ };
+
+ const onTextAlignChange = (textAlign: unknown) => {
+ onChange({ ...value, textConfig: { ...value.textConfig, textAlign: textAlign as TextAlignment } });
+ };
+
+ const onTextBaselineChange = (textBaseline: unknown) => {
+ onChange({ ...value, textConfig: { ...value.textConfig, textBaseline: textBaseline as TextBaseline } });
+ };
+
+ const hasTextLabel = Boolean(value.text?.fixed || value.text?.field);
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {(value.text?.fixed || value.text?.field) && (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ )}
+ >
+ );
+};
diff --git a/public/app/plugins/panel/geomap/layers/data/markersLayer.tsx b/public/app/plugins/panel/geomap/layers/data/markersLayer.tsx
index 1e7302d0660..f46ce0437f6 100644
--- a/public/app/plugins/panel/geomap/layers/data/markersLayer.tsx
+++ b/public/app/plugins/panel/geomap/layers/data/markersLayer.tsx
@@ -12,19 +12,14 @@ import { Point } from 'ol/geom';
import * as layer from 'ol/layer';
import * as source from 'ol/source';
import { dataFrameToPoints, getLocationMatchers } from '../../utils/location';
-import {
- getScaledDimension,
- getColorDimension,
- ResourceFolderName,
- getTextDimension,
-} from 'app/features/dimensions';
-import { ScaleDimensionEditor, ColorDimensionEditor, ResourceDimensionEditor, TextDimensionEditor } from 'app/features/dimensions/editors';
+import { getScaledDimension, getColorDimension, getTextDimension } from 'app/features/dimensions';
import { ObservablePropsWrapper } from '../../components/ObservablePropsWrapper';
import { MarkersLegend, MarkersLegendProps } from './MarkersLegend';
import { ReplaySubject } from 'rxjs';
import { FeaturesStylesBuilderConfig, getFeatures } from '../../utils/getFeatures';
import { getMarkerMaker } from '../../style/markers';
-import { defaultStyleConfig, StyleConfig, TextAlignment, TextBaseline } from '../../style/types';
+import { defaultStyleConfig, StyleConfig } from '../../style/types';
+import { StyleEditor } from './StyleEditor';
// Configuration options for Circle overlays
export interface MarkersConfig {
@@ -79,7 +74,8 @@ export const markersLayer: MapLayerRegistryItem = {
}
const style = config.style ?? defaultStyleConfig;
- const markerMaker = await getMarkerMaker(style.symbol?.fixed);
+ const hasTextLabel = Boolean(style.text?.fixed || style.text?.field);
+ const markerMaker = await getMarkerMaker(style.symbol?.fixed, hasTextLabel);
return {
init: () => vectorLayer,
@@ -100,7 +96,10 @@ export const markersLayer: MapLayerRegistryItem = {
const colorDim = getColorDimension(frame, style.color ?? defaultStyleConfig.color, theme);
const sizeDim = getScaledDimension(frame, style.size ?? defaultStyleConfig.size);
- const textDim = style?.text && getTextDimension(frame, style.text);
+ let textDim = undefined;
+ if (style?.text && (style.text.field || style.text.fixed)) {
+ textDim = getTextDimension(frame, style.text);
+ }
const opacity = style?.opacity ?? defaultStyleConfig.opacity;
const featureDimensionConfig: FeaturesStylesBuilderConfig = {
@@ -137,95 +136,13 @@ export const markersLayer: MapLayerRegistryItem = {
registerOptionsUI: (builder) => {
builder
.addCustomEditor({
- id: 'config.style.size',
- path: 'config.style.size',
- name: 'Marker Size',
- editor: ScaleDimensionEditor,
- settings: {
- min: 1,
- max: 100, // possible in the UI
- },
- defaultValue: defaultOptions.style.size,
- })
- .addCustomEditor({
- id: 'config.style.symbol',
- path: 'config.style.symbol',
- name: 'Marker Symbol',
- editor: ResourceDimensionEditor,
- defaultValue: defaultOptions.style.symbol,
- settings: {
- resourceType: 'icon',
- showSourceRadio: false,
- folderName: ResourceFolderName.Marker,
- },
- })
- .addCustomEditor({
- id: 'config.style.color',
- path: 'config.style.color',
- name: 'Marker Color',
- editor: ColorDimensionEditor,
+ id: 'config.style',
+ path: 'config.style',
+ name: 'Styles',
+ editor: StyleEditor,
settings: {},
- defaultValue: defaultOptions.style.color,
+ defaultValue: defaultOptions.style,
})
- .addSliderInput({
- path: 'config.style.opacity',
- name: 'Fill opacity',
- defaultValue: defaultOptions.style.opacity,
- settings: {
- min: 0,
- max: 1,
- step: 0.1,
- },
- })
- .addCustomEditor({
- id: 'config.style.text',
- path: 'config.style.text',
- name: 'Text label',
- editor: TextDimensionEditor,
- defaultValue: defaultOptions.style.text,
- })
- .addNumberInput({
- name: 'Text font size',
- path: 'config.style.textConfig.fontSize',
- defaultValue: defaultOptions.style.textConfig?.fontSize,
- })
- .addNumberInput({
- name: 'Text X offset',
- path: 'config.style.textConfig.offsetX',
- defaultValue: 0,
- })
- .addNumberInput({
- name: 'Text Y offset',
- path: 'config.style.textConfig.offsetY',
- defaultValue: 0,
- })
- .addRadio({
- name: 'Text align',
- path: 'config.style.textConfig.textAlign',
- description: '',
- defaultValue: defaultOptions.style.textConfig?.textAlign,
- settings: {
- options: [
- { value: TextAlignment.Left, label: TextAlignment.Left },
- { value: TextAlignment.Center, label: TextAlignment.Center },
- { value: TextAlignment.Right, label: TextAlignment.Right },
- ],
- },
- })
- .addRadio({
- name: 'Text baseline',
- path: 'config.style.textConfig.textBaseline',
- description: '',
- defaultValue: defaultOptions.style.textConfig?.textBaseline,
- settings: {
- options: [
- { value: TextBaseline.Top, label: TextBaseline.Top },
- { value: TextBaseline.Middle, label: TextBaseline.Middle },
- { value: TextBaseline.Bottom, label: TextBaseline.Bottom },
- ],
- },
- })
- // baseline?: 'bottom' | 'top' | 'middle';
.addBooleanSwitch({
path: 'config.showLegend',
name: 'Show legend',
diff --git a/public/app/plugins/panel/geomap/migrations.test.ts b/public/app/plugins/panel/geomap/migrations.test.ts
index fa53e7d972f..b866b6cf6b7 100644
--- a/public/app/plugins/panel/geomap/migrations.test.ts
+++ b/public/app/plugins/panel/geomap/migrations.test.ts
@@ -162,6 +162,8 @@ describe('geomap migrations', () => {
},
"textConfig": Object {
"fontSize": 12,
+ "offsetX": 0,
+ "offsetY": 0,
"textAlign": "center",
"textBaseline": "middle",
},
diff --git a/public/app/plugins/panel/geomap/style/markers.ts b/public/app/plugins/panel/geomap/style/markers.ts
index ca83d5fabec..dc0a67f045c 100644
--- a/public/app/plugins/panel/geomap/style/markers.ts
+++ b/public/app/plugins/panel/geomap/style/markers.ts
@@ -41,19 +41,29 @@ export function getFillColor(cfg: StyleConfigValues) {
}
const textLabel = (cfg: StyleConfigValues) => {
+ if (!cfg.text) {
+ return undefined;
+ }
+
const fontFamily = config.theme2.typography.fontFamily;
const textConfig = {
...defaultStyleConfig.textConfig,
...cfg.textConfig,
};
return new Text({
- text: cfg.text ?? '?',
+ text: cfg.text,
fill: new Fill({ color: cfg.color ?? defaultStyleConfig.color.fixed }),
font: `normal ${textConfig.fontSize}px ${fontFamily}`,
...textConfig,
});
};
+export const textMarker = (cfg: StyleConfigValues) => {
+ return new Style({
+ text: textLabel(cfg),
+ });
+};
+
export const circleMarker = (cfg: StyleConfigValues) => {
return new Style({
image: new Circle({
@@ -227,9 +237,9 @@ export function getMarkerAsPath(shape?: string): string | undefined {
}
// Will prepare symbols as necessary
-export async function getMarkerMaker(symbol?: string): Promise {
+export async function getMarkerMaker(symbol?: string, hasTextLabel?: boolean): Promise {
if (!symbol) {
- return circleMarker;
+ return hasTextLabel ? textMarker : circleMarker;
}
let maker = markerMakers.getIfExists(symbol);
@@ -274,6 +284,6 @@ export async function getMarkerMaker(symbol?: string): Promise {
return maker.make;
}
- // defatult to showing a circle
+ // default to showing a circle
return errorMarker;
}
diff --git a/public/app/plugins/panel/geomap/style/types.ts b/public/app/plugins/panel/geomap/style/types.ts
index bc4465310c0..d40cbb1ffaa 100644
--- a/public/app/plugins/panel/geomap/style/types.ts
+++ b/public/app/plugins/panel/geomap/style/types.ts
@@ -62,6 +62,8 @@ export const defaultStyleConfig = Object.freeze({
fontSize: 12,
textAlign: TextAlignment.Center,
textBaseline: TextBaseline.Middle,
+ offsetX: 0,
+ offsetY: 0,
},
});