mirror of https://github.com/grafana/grafana.git
				
				
				
			Geomap: Add variable support for GeoJSON url (#104587)
* Geomap: Add variable support for GeoJSON url * Add a check for variable dependency * Add tests for hasVariableDependencies function * Update docs * Update docs/sources/panels-visualizations/visualizations/geomap/index.md Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> --------- Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com>
This commit is contained in:
		
							parent
							
								
									a049ddece7
								
							
						
					
					
						commit
						3dc5742a75
					
				|  | @ -313,7 +313,7 @@ The GeoJSON layer allows you to select and load a static GeoJSON file from the f | |||
| <!-- prettier-ignore-start --> | ||||
| | Option | Description | | ||||
| | ------ | ----------- | | ||||
| | GeoJSON URL | Provides a choice of GeoJSON files that ship with Grafana. | | ||||
| | GeoJSON URL | Provides a choice of GeoJSON files that are included with Grafana. You can also enter a URL manually, which supports variables. | | ||||
| | Default Style | Controls which styles to apply when no rules above match.<ul><li>**Color** - configures the color of the default style</li><li>**Opacity** - configures the default opacity</li></ul> | | ||||
| | Style Rules | Apply styles based on feature properties <ul><li>**Rule** - allows you to select a _feature_, _condition_, and _value_ from the GeoJSON file in order to define a rule. The trash bin icon can be used to delete the current rule.</li><li>**Color** - configures the color of the style for the current rule</li><li>**Opacity** - configures the transparency level for the current rule</li> | | ||||
| | Display tooltip | Allows you to toggle tooltips for the layer. | | ||||
|  |  | |||
|  | @ -15,6 +15,8 @@ import { Subscription } from 'rxjs'; | |||
| import { DataHoverEvent, PanelData, PanelProps } from '@grafana/data'; | ||||
| import { config } from '@grafana/runtime'; | ||||
| import { PanelContext, PanelContextRoot } from '@grafana/ui'; | ||||
| import { appEvents } from 'app/core/app_events'; | ||||
| import { VariablesChanged } from 'app/features/variables/types'; | ||||
| import { PanelEditExitedEvent } from 'app/types/events'; | ||||
| 
 | ||||
| import { GeomapOverlay, OverlayProps } from './GeomapOverlay'; | ||||
|  | @ -31,7 +33,7 @@ import { getActions } from './utils/actions'; | |||
| import { getLayersExtent } from './utils/getLayersExtent'; | ||||
| import { applyLayerFilter, initLayer } from './utils/layers'; | ||||
| import { pointerClickListener, pointerMoveListener, setTooltipListeners } from './utils/tooltip'; | ||||
| import { updateMap, getNewOpenLayersMap, notifyPanelEditor } from './utils/utils'; | ||||
| import { updateMap, getNewOpenLayersMap, notifyPanelEditor, hasVariableDependencies } from './utils/utils'; | ||||
| import { centerPointRegistry, MapCenterID } from './view'; | ||||
| 
 | ||||
| // Allows multiple panels to share the same view instance
 | ||||
|  | @ -72,6 +74,25 @@ export class GeomapPanel extends Component<Props, State> { | |||
|         } | ||||
|       }) | ||||
|     ); | ||||
|     // Subscribe to variable changes
 | ||||
|     this.subs.add( | ||||
|       appEvents.subscribe(VariablesChanged, () => { | ||||
|         if (this.mapDiv) { | ||||
|           // Check if any of the map's layers are dependent on variables
 | ||||
|           const hasDependencies = this.layers.some((layer) => { | ||||
|             const config = layer.options.config; | ||||
|             if (!config || typeof config !== 'object') { | ||||
|               return false; | ||||
|             } | ||||
|             return hasVariableDependencies(config); | ||||
|           }); | ||||
| 
 | ||||
|           if (hasDependencies) { | ||||
|             this.initMapRef(this.mapDiv); | ||||
|           } | ||||
|         } | ||||
|       }) | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   componentDidMount() { | ||||
|  |  | |||
|  | @ -9,6 +9,7 @@ import { ReplaySubject } from 'rxjs'; | |||
| import { map as rxjsmap, first } from 'rxjs/operators'; | ||||
| 
 | ||||
| import { MapLayerRegistryItem, MapLayerOptions, GrafanaTheme2, EventBus } from '@grafana/data'; | ||||
| import { getTemplateSrv } from '@grafana/runtime'; | ||||
| import { ComparisonOperation } from '@grafana/schema'; | ||||
| 
 | ||||
| import { GeomapStyleRulesEditor } from '../../editor/GeomapStyleRulesEditor'; | ||||
|  | @ -75,8 +76,11 @@ export const geojsonLayer: MapLayerRegistryItem<GeoJSONMapperConfig> = { | |||
|   create: async (map: Map, options: MapLayerOptions<GeoJSONMapperConfig>, eventBus: EventBus, theme: GrafanaTheme2) => { | ||||
|     const config = { ...defaultOptions, ...options.config }; | ||||
| 
 | ||||
|     // Interpolate variables in the URL
 | ||||
|     const interpolatedUrl = getTemplateSrv().replace(config.src || ''); | ||||
| 
 | ||||
|     const source = new VectorSource({ | ||||
|       url: config.src, | ||||
|       url: interpolatedUrl, | ||||
|       format: new GeoJSON(), | ||||
|     }); | ||||
| 
 | ||||
|  | @ -194,6 +198,7 @@ export const geojsonLayer: MapLayerRegistryItem<GeoJSONMapperConfig> = { | |||
|             settings: { | ||||
|               options: getPublicGeoJSONFiles() ?? [], | ||||
|               allowCustomValue: true, | ||||
|               supportVariables: true, | ||||
|             }, | ||||
|             defaultValue: defaultOptions.src, | ||||
|           }) | ||||
|  |  | |||
|  | @ -0,0 +1,101 @@ | |||
| import { getTemplateSrv } from '@grafana/runtime'; | ||||
| 
 | ||||
| // Mock the config module to avoid undefined panels error
 | ||||
| jest.mock('app/core/config', () => ({ | ||||
|   config: { | ||||
|     panels: { | ||||
|       debug: { | ||||
|         state: 'alpha', | ||||
|       }, | ||||
|     }, | ||||
|   }, | ||||
| })); | ||||
| 
 | ||||
| // Mock the dimensions module since it's imported by utils.ts
 | ||||
| jest.mock('app/features/dimensions', () => ({ | ||||
|   getColorDimension: jest.fn(), | ||||
|   getScalarDimension: jest.fn(), | ||||
|   getScaledDimension: jest.fn(), | ||||
|   getTextDimension: jest.fn(), | ||||
| })); | ||||
| 
 | ||||
| // Mock the grafana datasource since it's imported by utils.ts
 | ||||
| jest.mock('app/plugins/datasource/grafana/datasource', () => ({ | ||||
|   getGrafanaDatasource: jest.fn(), | ||||
| })); | ||||
| 
 | ||||
| // Mock the template service
 | ||||
| jest.mock('@grafana/runtime', () => ({ | ||||
|   getTemplateSrv: jest.fn(), | ||||
| })); | ||||
| 
 | ||||
| import { hasVariableDependencies } from './utils'; | ||||
| 
 | ||||
| describe('hasVariableDependencies', () => { | ||||
|   beforeEach(() => { | ||||
|     jest.clearAllMocks(); | ||||
|   }); | ||||
| 
 | ||||
|   it('should return true when object contains existing template variables', () => { | ||||
|     const availableVariables = [{ name: 'variable' }]; | ||||
|     const mockTemplateSrv = { | ||||
|       containsTemplate: jest.fn().mockImplementation((str) => { | ||||
|         // Check if any of the available variables are in the string
 | ||||
|         return availableVariables.some((v) => str.includes(`$${v.name}`)); | ||||
|       }), | ||||
|       getVariables: jest.fn().mockReturnValue(availableVariables), | ||||
|     }; | ||||
|     (getTemplateSrv as jest.Mock).mockReturnValue(mockTemplateSrv); | ||||
| 
 | ||||
|     const obj = { key: '$variable' }; | ||||
|     expect(hasVariableDependencies(obj)).toBe(true); | ||||
|     expect(mockTemplateSrv.containsTemplate).toHaveBeenCalledWith(JSON.stringify(obj)); | ||||
|   }); | ||||
| 
 | ||||
|   it('should return false when object contains non-existent template variables', () => { | ||||
|     const availableVariables = [{ name: 'variable' }]; | ||||
|     const mockTemplateSrv = { | ||||
|       containsTemplate: jest.fn().mockImplementation((str) => { | ||||
|         return availableVariables.some((v) => str.includes(`$${v.name}`)); | ||||
|       }), | ||||
|       getVariables: jest.fn().mockReturnValue(availableVariables), | ||||
|     }; | ||||
|     (getTemplateSrv as jest.Mock).mockReturnValue(mockTemplateSrv); | ||||
| 
 | ||||
|     const obj = { key: '$nonexistent' }; | ||||
|     expect(hasVariableDependencies(obj)).toBe(false); | ||||
|     expect(mockTemplateSrv.containsTemplate).toHaveBeenCalledWith(JSON.stringify(obj)); | ||||
|   }); | ||||
| 
 | ||||
|   it('should return false when object does not contain template variables', () => { | ||||
|     const mockTemplateSrv = { | ||||
|       containsTemplate: jest.fn().mockReturnValue(false), | ||||
|       getVariables: jest.fn().mockReturnValue([]), | ||||
|     }; | ||||
|     (getTemplateSrv as jest.Mock).mockReturnValue(mockTemplateSrv); | ||||
| 
 | ||||
|     const obj = { key: 'static value' }; | ||||
|     expect(hasVariableDependencies(obj)).toBe(false); | ||||
|     expect(mockTemplateSrv.containsTemplate).toHaveBeenCalledWith(JSON.stringify(obj)); | ||||
|   }); | ||||
| 
 | ||||
|   it('should handle nested objects with existing template variables', () => { | ||||
|     const availableVariables = [{ name: 'variable' }]; | ||||
|     const mockTemplateSrv = { | ||||
|       containsTemplate: jest.fn().mockImplementation((str) => { | ||||
|         return availableVariables.some((v) => str.includes(`$${v.name}`)); | ||||
|       }), | ||||
|       getVariables: jest.fn().mockReturnValue(availableVariables), | ||||
|     }; | ||||
|     (getTemplateSrv as jest.Mock).mockReturnValue(mockTemplateSrv); | ||||
| 
 | ||||
|     const obj = { | ||||
|       key: 'static value', | ||||
|       nested: { | ||||
|         anotherKey: '$variable', | ||||
|       }, | ||||
|     }; | ||||
|     expect(hasVariableDependencies(obj)).toBe(true); | ||||
|     expect(mockTemplateSrv.containsTemplate).toHaveBeenCalledWith(JSON.stringify(obj)); | ||||
|   }); | ||||
| }); | ||||
|  | @ -2,6 +2,7 @@ import { Map as OpenLayersMap } from 'ol'; | |||
| import { defaults as interactionDefaults } from 'ol/interaction'; | ||||
| 
 | ||||
| import { DataFrame, GrafanaTheme2, SelectableValue } from '@grafana/data'; | ||||
| import { getTemplateSrv } from '@grafana/runtime'; | ||||
| import { getColorDimension, getScalarDimension, getScaledDimension, getTextDimension } from 'app/features/dimensions'; | ||||
| import { getGrafanaDatasource } from 'app/plugins/datasource/grafana/datasource'; | ||||
| 
 | ||||
|  | @ -73,6 +74,15 @@ async function initGeojsonFiles() { | |||
|   } | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Checks if an object contains any Grafana template variables | ||||
|  * @param obj - The object to check for variables | ||||
|  * @returns true if the object contains any template variables | ||||
|  */ | ||||
| export const hasVariableDependencies = (obj: object): boolean => { | ||||
|   return getTemplateSrv().containsTemplate(JSON.stringify(obj)); | ||||
| }; | ||||
| 
 | ||||
| export const getNewOpenLayersMap = (panel: GeomapPanel, options: Options, div: HTMLDivElement) => { | ||||
|   const view = panel.initMapView(options.view); | ||||
|   return (panel.map = new OpenLayersMap({ | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue