diff --git a/public/app/features/dimensions/editors/ResourceDimensionEditor.tsx b/public/app/features/dimensions/editors/ResourceDimensionEditor.tsx
index 801c15ca24f..74871153863 100644
--- a/public/app/features/dimensions/editors/ResourceDimensionEditor.tsx
+++ b/public/app/features/dimensions/editors/ResourceDimensionEditor.tsx
@@ -4,6 +4,7 @@ import { ResourceDimensionConfig, ResourceDimensionMode, ResourceDimensionOption
import { InlineField, InlineFieldRow, RadioButtonGroup, Button, Modal, Input } from '@grafana/ui';
import { FieldNamePicker } from '../../../../../packages/grafana-ui/src/components/MatchersUI/FieldNamePicker';
import { ResourcePicker } from './ResourcePicker';
+import { ResourceFolderName } from '..';
const resourceOptions = [
{ label: 'Fixed', value: ResourceDimensionMode.Fixed, description: 'Fixed value' },
@@ -58,21 +59,24 @@ export const ResourceDimensionEditor: FC<
}, []);
const mode = value?.mode ?? ResourceDimensionMode.Fixed;
+ const showSourceRadio = item.settings?.showSourceRadio ?? true;
const mediaType = item.settings?.resourceType ?? 'icon';
+ const folderName = item.settings?.folderName ?? ResourceFolderName.Icon;
return (
<>
{isOpen && (
setOpen(false)} closeOnEscape>
-
+
)}
-
-
-
-
-
-
+ {showSourceRadio && (
+
+
+
+
+
+ )}
{mode !== ResourceDimensionMode.Fixed && (
diff --git a/public/app/features/dimensions/editors/ResourcePicker.tsx b/public/app/features/dimensions/editors/ResourcePicker.tsx
index 0f7e27363f1..4672070bb1d 100644
--- a/public/app/features/dimensions/editors/ResourcePicker.tsx
+++ b/public/app/features/dimensions/editors/ResourcePicker.tsx
@@ -18,11 +18,13 @@ import { css } from '@emotion/css';
import { getPublicOrAbsoluteUrl } from '../resource';
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
import { FileElement, GrafanaDatasource } from 'app/plugins/datasource/grafana/datasource';
+import { ResourceFolderName } from '..';
interface Props {
value?: string; //img/icons/unicons/0-plus.svg
onChange: (value?: string) => void;
mediaType: 'icon' | 'image';
+ folderName: ResourceFolderName;
}
interface ResourceItem {
@@ -33,12 +35,13 @@ interface ResourceItem {
}
export function ResourcePicker(props: Props) {
- const { value, onChange, mediaType } = props;
- const folders = (mediaType === 'icon' ? ['img/icons/unicons', 'img/icons/iot'] : ['img/bg']).map((v) => ({
+ const { value, onChange, mediaType, folderName } = props;
+ const folders = getFolders(mediaType).map((v) => ({
label: v,
value: v,
}));
- const folderOfCurrentValue = value ? folders.filter((folder) => value.indexOf(folder.value) > -1)[0] : folders[0];
+
+ const folderOfCurrentValue = value || folderName ? folderIfExists(folders, value ?? folderName) : folders[0];
const [currentFolder, setCurrentFolder] = useState>(folderOfCurrentValue);
const [tabs, setTabs] = useState([
{ label: 'Select', active: true },
@@ -169,3 +172,15 @@ const getStyles = stylesFactory((theme: GrafanaTheme2) => {
`,
};
});
+
+const getFolders = (mediaType: 'icon' | 'image') => {
+ if (mediaType === 'icon') {
+ return [ResourceFolderName.Icon, ResourceFolderName.IOT, ResourceFolderName.Marker];
+ } else {
+ return [ResourceFolderName.BG];
+ }
+};
+
+const folderIfExists = (folders: Array<{ label: string; value: string }>, path: string) => {
+ return folders.filter((folder) => path.indexOf(folder.value) > -1)[0] ?? folders[0];
+};
diff --git a/public/app/features/dimensions/types.ts b/public/app/features/dimensions/types.ts
index bb028304dcb..097e244cb46 100644
--- a/public/app/features/dimensions/types.ts
+++ b/public/app/features/dimensions/types.ts
@@ -71,6 +71,8 @@ export interface ColorDimensionConfig extends BaseDimensionConfig {}
/** Places that use the value */
export interface ResourceDimensionOptions {
resourceType: 'icon' | 'image';
+ folderName?: ResourceFolderName;
+ showSourceRadio?: boolean;
}
export enum ResourceDimensionMode {
@@ -84,3 +86,10 @@ export enum ResourceDimensionMode {
export interface ResourceDimensionConfig extends BaseDimensionConfig {
mode: ResourceDimensionMode;
}
+
+export enum ResourceFolderName {
+ Icon = 'img/icons/unicons',
+ IOT = 'img/icons/iot',
+ Marker = 'img/icons/marker',
+ BG = 'img/bg',
+}
diff --git a/public/app/plugins/panel/geomap/layers/data/markersLayer.tsx b/public/app/plugins/panel/geomap/layers/data/markersLayer.tsx
index 6e1b6534773..666bb678bcb 100644
--- a/public/app/plugins/panel/geomap/layers/data/markersLayer.tsx
+++ b/public/app/plugins/panel/geomap/layers/data/markersLayer.tsx
@@ -11,6 +11,7 @@ import Feature from 'ol/Feature';
import { Point } from 'ol/geom';
import * as layer from 'ol/layer';
import * as source from 'ol/source';
+import * as style from 'ol/style';
import tinycolor from 'tinycolor2';
import { dataFrameToPoints, getLocationMatchers } from '../../utils/location';
@@ -19,11 +20,15 @@ import {
ScaleDimensionConfig,
getScaledDimension,
getColorDimension,
+ ResourceDimensionConfig,
+ ResourceDimensionMode,
+ ResourceFolderName,
+ getPublicOrAbsoluteUrl,
} from 'app/features/dimensions';
-import { ScaleDimensionEditor, ColorDimensionEditor } from 'app/features/dimensions/editors';
+import { ScaleDimensionEditor, ColorDimensionEditor, ResourceDimensionEditor } from 'app/features/dimensions/editors';
import { ObservablePropsWrapper } from '../../components/ObservablePropsWrapper';
import { MarkersLegend, MarkersLegendProps } from './MarkersLegend';
-import { circleMarker, markerMakers } from '../../utils/regularShapes';
+import { StyleMaker, getMarkerFromPath, MarkerShapePath } from '../../utils/regularShapes';
import { ReplaySubject } from 'rxjs';
// Configuration options for Circle overlays
@@ -31,13 +36,15 @@ export interface MarkersConfig {
size: ScaleDimensionConfig;
color: ColorDimensionConfig;
fillOpacity: number;
- shape?: string;
showLegend?: boolean;
+ markerSymbol: ResourceDimensionConfig;
}
+const DEFAULT_SIZE = 5;
+
const defaultOptions: MarkersConfig = {
size: {
- fixed: 5,
+ fixed: DEFAULT_SIZE,
min: 2,
max: 15,
},
@@ -45,8 +52,11 @@ const defaultOptions: MarkersConfig = {
fixed: 'dark-green', // picked from theme
},
fillOpacity: 0.4,
- shape: 'circle',
showLegend: true,
+ markerSymbol: {
+ mode: ResourceDimensionMode.Fixed,
+ fixed: MarkerShapePath.Circle,
+ },
};
export const MARKERS_LAYER_ID = 'markers';
@@ -88,7 +98,6 @@ export const markersLayer: MapLayerRegistryItem = {
if (config.showLegend) {
legend = ;
}
- const shape = markerMakers.getIfExists(config.shape) ?? circleMarker;
return {
init: () => vectorLayer,
@@ -98,6 +107,24 @@ export const markersLayer: MapLayerRegistryItem = {
return; // ignore empty
}
+ const markerPath =
+ getPublicOrAbsoluteUrl(config.markerSymbol?.fixed) ?? getPublicOrAbsoluteUrl(MarkerShapePath.Circle);
+
+ const marker = getMarkerFromPath(config.markerSymbol?.fixed);
+
+ const makeIconStyle = (color: string, fillColor: string, radius: number) => {
+ return new style.Style({
+ image: new style.Icon({
+ src: markerPath,
+ color,
+ // opacity,
+ scale: (DEFAULT_SIZE + radius) / 100,
+ }),
+ });
+ };
+
+ const shape: StyleMaker = marker?.make ?? makeIconStyle;
+
const features: Feature[] = [];
for (const frame of data.series) {
@@ -126,8 +153,7 @@ export const markersLayer: MapLayerRegistryItem = {
frame,
rowIndex: i,
});
-
- dot.setStyle(shape!.make(color, fillColor, radius));
+ dot.setStyle(shape(color, fillColor, radius));
features.push(dot);
}
@@ -150,17 +176,6 @@ export const markersLayer: MapLayerRegistryItem = {
// Marker overlay options
registerOptionsUI: (builder) => {
builder
- .addCustomEditor({
- id: 'config.color',
- path: 'config.color',
- name: 'Marker Color',
- editor: ColorDimensionEditor,
- settings: {},
- defaultValue: {
- // Configured values
- fixed: 'grey',
- },
- })
.addCustomEditor({
id: 'config.size',
path: 'config.size',
@@ -172,18 +187,33 @@ export const markersLayer: MapLayerRegistryItem = {
},
defaultValue: {
// Configured values
- fixed: 5,
+ fixed: DEFAULT_SIZE,
min: 1,
max: 20,
},
})
- .addSelect({
- path: 'config.shape',
- name: 'Marker Shape',
+ .addCustomEditor({
+ id: 'config.markerSymbol',
+ path: 'config.markerSymbol',
+ name: 'Marker Symbol',
+ editor: ResourceDimensionEditor,
+ defaultValue: defaultOptions.markerSymbol,
settings: {
- options: markerMakers.selectOptions().options,
+ resourceType: 'icon',
+ showSourceRadio: false,
+ folderName: ResourceFolderName.Marker,
+ },
+ })
+ .addCustomEditor({
+ id: 'config.color',
+ path: 'config.color',
+ name: 'Marker Color',
+ editor: ColorDimensionEditor,
+ settings: {},
+ defaultValue: {
+ // Configured values
+ fixed: 'grey',
},
- defaultValue: 'circle',
})
.addSliderInput({
path: 'config.fillOpacity',
@@ -194,7 +224,6 @@ export const markersLayer: MapLayerRegistryItem = {
max: 1,
step: 0.1,
},
- showIf: (cfg) => markerMakers.getIfExists((cfg as any).config?.shape)?.hasFill,
})
.addBooleanSwitch({
path: 'config.showLegend',
diff --git a/public/app/plugins/panel/geomap/migrations.test.ts b/public/app/plugins/panel/geomap/migrations.test.ts
index fbe64d78869..c4024e5acdc 100644
--- a/public/app/plugins/panel/geomap/migrations.test.ts
+++ b/public/app/plugins/panel/geomap/migrations.test.ts
@@ -1,6 +1,5 @@
import { PanelModel, FieldConfigSource } from '@grafana/data';
-import { mapPanelChangedHandler } from './migrations';
-
+import { mapMigrationHandler, mapPanelChangedHandler } from './migrations';
describe('Worldmap Migrations', () => {
let prevFieldConfig: FieldConfigSource;
@@ -106,3 +105,169 @@ const simpleWorldmapConfig = {
valueName: 'total',
datasource: null,
};
+
+describe('geomap migrations', () => {
+ it('updates marker', () => {
+ const panel = {
+ id: 2,
+ gridPos: {
+ h: 9,
+ w: 12,
+ x: 0,
+ y: 0,
+ },
+ type: 'geomap',
+ title: 'Panel Title',
+ fieldConfig: {
+ defaults: {
+ thresholds: {
+ mode: 'absolute',
+ steps: [
+ {
+ color: 'green',
+ value: null,
+ },
+ {
+ color: 'red',
+ value: 80,
+ },
+ ],
+ },
+ mappings: [],
+ color: {
+ mode: 'thresholds',
+ },
+ },
+ overrides: [],
+ },
+ options: {
+ view: {
+ id: 'zero',
+ lat: 0,
+ lon: 0,
+ zoom: 1,
+ },
+ basemap: {
+ type: 'default',
+ config: {},
+ },
+ layers: [
+ {
+ config: {
+ color: {
+ fixed: 'dark-green',
+ },
+ fillOpacity: 0.4,
+ markerSymbol: {
+ fixed: '',
+ mode: 'fixed',
+ },
+ shape: 'circle',
+ showLegend: true,
+ size: {
+ fixed: 5,
+ max: 15,
+ min: 2,
+ },
+ },
+ location: {
+ mode: 'auto',
+ },
+ type: 'markers',
+ },
+ ],
+ controls: {
+ showZoom: true,
+ mouseWheelZoom: true,
+ showAttribution: true,
+ showScale: false,
+ showDebug: false,
+ },
+ },
+ pluginVersion: '8.3.0-pre',
+ datasource: null,
+ } as PanelModel;
+ panel.options = mapMigrationHandler(panel);
+
+ expect(panel).toMatchInlineSnapshot(`
+ Object {
+ "datasource": null,
+ "fieldConfig": Object {
+ "defaults": Object {
+ "color": Object {
+ "mode": "thresholds",
+ },
+ "mappings": Array [],
+ "thresholds": Object {
+ "mode": "absolute",
+ "steps": Array [
+ Object {
+ "color": "green",
+ "value": null,
+ },
+ Object {
+ "color": "red",
+ "value": 80,
+ },
+ ],
+ },
+ },
+ "overrides": Array [],
+ },
+ "gridPos": Object {
+ "h": 9,
+ "w": 12,
+ "x": 0,
+ "y": 0,
+ },
+ "id": 2,
+ "options": Object {
+ "basemap": Object {
+ "config": Object {},
+ "type": "default",
+ },
+ "controls": Object {
+ "mouseWheelZoom": true,
+ "showAttribution": true,
+ "showDebug": false,
+ "showScale": false,
+ "showZoom": true,
+ },
+ "layers": Array [
+ Object {
+ "config": Object {
+ "color": Object {
+ "fixed": "dark-green",
+ },
+ "fillOpacity": 0.4,
+ "markerSymbol": Object {
+ "fixed": "img/icons/marker/circle.svg",
+ "mode": "fixed",
+ },
+ "showLegend": true,
+ "size": Object {
+ "fixed": 5,
+ "max": 15,
+ "min": 2,
+ },
+ },
+ "location": Object {
+ "mode": "auto",
+ },
+ "type": "markers",
+ },
+ ],
+ "view": Object {
+ "id": "zero",
+ "lat": 0,
+ "lon": 0,
+ "zoom": 1,
+ },
+ },
+ "pluginVersion": "8.3.0-pre",
+ "title": "Panel Title",
+ "type": "geomap",
+ }
+ `);
+ });
+});
diff --git a/public/app/plugins/panel/geomap/migrations.ts b/public/app/plugins/panel/geomap/migrations.ts
index 26a2fbdd880..f832668ceff 100644
--- a/public/app/plugins/panel/geomap/migrations.ts
+++ b/public/app/plugins/panel/geomap/migrations.ts
@@ -1,5 +1,6 @@
-import { FieldConfigSource, PanelTypeChangedHandler, Threshold, ThresholdsMode } from '@grafana/data';
+import { FieldConfigSource, PanelModel, PanelTypeChangedHandler, Threshold, ThresholdsMode } from '@grafana/data';
import { GeomapPanelOptions } from './types';
+import { markerMakers } from './utils/regularShapes';
import { MapCenterID } from './view';
/**
@@ -97,3 +98,27 @@ function asNumber(v: any): number | undefined {
const num = +v;
return isNaN(num) ? undefined : num;
}
+
+export const mapMigrationHandler = (panel: PanelModel): Partial => {
+ const pluginVersion = panel?.pluginVersion;
+ if (pluginVersion?.startsWith('8.1') || pluginVersion?.startsWith('8.2') || pluginVersion?.startsWith('8.3')) {
+ if (panel.options?.layers?.length > 0) {
+ const layer = panel.options.layers[0];
+ if (layer?.type === 'markers') {
+ const shape = layer?.config?.shape;
+ if (shape) {
+ const marker = markerMakers.getIfExists(shape);
+ if (marker?.aliasIds && marker.aliasIds?.length > 0) {
+ layer.config.markerSymbol = {
+ fixed: marker.aliasIds[0],
+ mode: 'fixed',
+ };
+ delete layer.config.shape;
+ }
+ return { ...panel.options, layers: Object.assign([], ...panel.options.layers, { 0: layer }) };
+ }
+ }
+ }
+ }
+ return panel.options;
+};
diff --git a/public/app/plugins/panel/geomap/module.tsx b/public/app/plugins/panel/geomap/module.tsx
index c7edc522803..73d7507086d 100644
--- a/public/app/plugins/panel/geomap/module.tsx
+++ b/public/app/plugins/panel/geomap/module.tsx
@@ -3,13 +3,14 @@ import { PanelPlugin } from '@grafana/data';
import { GeomapPanel } from './GeomapPanel';
import { MapViewEditor } from './editor/MapViewEditor';
import { defaultView, GeomapPanelOptions } from './types';
-import { mapPanelChangedHandler } from './migrations';
+import { mapPanelChangedHandler, mapMigrationHandler } from './migrations';
import { getLayerEditor } from './editor/layerEditor';
import { config } from '@grafana/runtime';
export const plugin = new PanelPlugin(GeomapPanel)
.setNoPadding()
.setPanelChangeHandler(mapPanelChangedHandler)
+ .setMigrationHandler(mapMigrationHandler)
.useFieldConfig()
.setPanelOptions((builder, context) => {
let category = ['Map view'];
diff --git a/public/app/plugins/panel/geomap/utils/regularShapes.ts b/public/app/plugins/panel/geomap/utils/regularShapes.ts
index 74091f93e49..454405dbbee 100644
--- a/public/app/plugins/panel/geomap/utils/regularShapes.ts
+++ b/public/app/plugins/panel/geomap/utils/regularShapes.ts
@@ -1,15 +1,38 @@
import { Fill, RegularShape, Stroke, Style, Circle } from 'ol/style';
import { Registry, RegistryItem } from '@grafana/data';
-interface MarkerMaker extends RegistryItem {
- make: (color: string, fillColor: string, radius: number) => Style;
+export type StyleMaker = (color: string, fillColor: string, radius: number, markerPath?: string) => Style;
+
+export interface MarkerMaker extends RegistryItem {
+ // path to icon that will be shown (but then replaced)
+ aliasIds: string[];
+ make: StyleMaker;
hasFill: boolean;
}
+export enum RegularShapeId {
+ Circle = 'circle',
+ Square = 'square',
+ Triangle = 'triangle',
+ Star = 'star',
+ Cross = 'cross',
+ X = 'x',
+}
+
+export enum MarkerShapePath {
+ Circle = 'img/icons/marker/circle.svg',
+ Square = 'img/icons/marker/square.svg',
+ Triangle = 'img/icons/marker/triangle.svg',
+ Star = 'img/icons/marker/star.svg',
+ Cross = 'img/icons/marker/cross.svg',
+ X = 'img/icons/marker/x-mark.svg',
+}
+
export const circleMarker: MarkerMaker = {
- id: 'circle',
+ id: RegularShapeId.Circle,
name: 'Circle',
hasFill: true,
+ aliasIds: [MarkerShapePath.Circle],
make: (color: string, fillColor: string, radius: number) => {
return new Style({
image: new Circle({
@@ -21,12 +44,13 @@ export const circleMarker: MarkerMaker = {
},
};
-export const markerMakers = new Registry(() => [
+const makers: MarkerMaker[] = [
circleMarker,
{
- id: 'square',
+ id: RegularShapeId.Square,
name: 'Square',
hasFill: true,
+ aliasIds: [MarkerShapePath.Square],
make: (color: string, fillColor: string, radius: number) => {
return new Style({
image: new RegularShape({
@@ -40,9 +64,10 @@ export const markerMakers = new Registry(() => [
},
},
{
- id: 'triangle',
+ id: RegularShapeId.Triangle,
name: 'Triangle',
hasFill: true,
+ aliasIds: [MarkerShapePath.Triangle],
make: (color: string, fillColor: string, radius: number) => {
return new Style({
image: new RegularShape({
@@ -57,9 +82,10 @@ export const markerMakers = new Registry(() => [
},
},
{
- id: 'star',
+ id: RegularShapeId.Star,
name: 'Star',
hasFill: true,
+ aliasIds: [MarkerShapePath.Star],
make: (color: string, fillColor: string, radius: number) => {
return new Style({
image: new RegularShape({
@@ -74,9 +100,10 @@ export const markerMakers = new Registry(() => [
},
},
{
- id: 'cross',
+ id: RegularShapeId.Cross,
name: 'Cross',
hasFill: false,
+ aliasIds: [MarkerShapePath.Cross],
make: (color: string, fillColor: string, radius: number) => {
return new Style({
image: new RegularShape({
@@ -91,9 +118,10 @@ export const markerMakers = new Registry(() => [
},
},
{
- id: 'x',
+ id: RegularShapeId.X,
name: 'X',
hasFill: false,
+ aliasIds: [MarkerShapePath.X],
make: (color: string, fillColor: string, radius: number) => {
return new Style({
image: new RegularShape({
@@ -107,4 +135,15 @@ export const markerMakers = new Registry(() => [
});
},
},
-]);
+];
+
+export const markerMakers = new Registry(() => makers);
+
+export const getMarkerFromPath = (svgPath: string): MarkerMaker | undefined => {
+ for (const [key, val] of Object.entries(MarkerShapePath)) {
+ if (val === svgPath) {
+ return markerMakers.getIfExists(key);
+ }
+ }
+ return undefined;
+};
diff --git a/public/img/icons/marker/circle.svg b/public/img/icons/marker/circle.svg
new file mode 100644
index 00000000000..afb4749ff10
--- /dev/null
+++ b/public/img/icons/marker/circle.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/img/icons/marker/cross.svg b/public/img/icons/marker/cross.svg
new file mode 100644
index 00000000000..399f74920d5
--- /dev/null
+++ b/public/img/icons/marker/cross.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/img/icons/marker/plane.svg b/public/img/icons/marker/plane.svg
new file mode 100644
index 00000000000..a49c2c502fb
--- /dev/null
+++ b/public/img/icons/marker/plane.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/img/icons/marker/square.svg b/public/img/icons/marker/square.svg
new file mode 100644
index 00000000000..856cd02fca3
--- /dev/null
+++ b/public/img/icons/marker/square.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/img/icons/marker/star.svg b/public/img/icons/marker/star.svg
new file mode 100644
index 00000000000..7fc7a836a7e
--- /dev/null
+++ b/public/img/icons/marker/star.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/img/icons/marker/triangle.svg b/public/img/icons/marker/triangle.svg
new file mode 100644
index 00000000000..9258e6e11a1
--- /dev/null
+++ b/public/img/icons/marker/triangle.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/img/icons/marker/x-mark.svg b/public/img/icons/marker/x-mark.svg
new file mode 100644
index 00000000000..f1432176da4
--- /dev/null
+++ b/public/img/icons/marker/x-mark.svg
@@ -0,0 +1 @@
+
\ No newline at end of file