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