diff --git a/docs/sources/panels-visualizations/visualizations/geomap/index.md b/docs/sources/panels-visualizations/visualizations/geomap/index.md
index 64b20d33dd7..27352e77caa 100644
--- a/docs/sources/panels-visualizations/visualizations/geomap/index.md
+++ b/docs/sources/panels-visualizations/visualizations/geomap/index.md
@@ -313,7 +313,7 @@ The GeoJSON layer allows you to select and load a static GeoJSON file from the f
| 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.
- **Color** - configures the color of the default style
- **Opacity** - configures the default opacity
|
| Style Rules | Apply styles based on feature properties - **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.
- **Color** - configures the color of the style for the current rule
- **Opacity** - configures the transparency level for the current rule
|
| Display tooltip | Allows you to toggle tooltips for the layer. |
diff --git a/public/app/plugins/panel/geomap/GeomapPanel.tsx b/public/app/plugins/panel/geomap/GeomapPanel.tsx
index bbbb0eb3510..48d0634c436 100644
--- a/public/app/plugins/panel/geomap/GeomapPanel.tsx
+++ b/public/app/plugins/panel/geomap/GeomapPanel.tsx
@@ -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 {
}
})
);
+ // 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() {
diff --git a/public/app/plugins/panel/geomap/layers/data/geojsonLayer.ts b/public/app/plugins/panel/geomap/layers/data/geojsonLayer.ts
index 176126f4183..03ff6603504 100644
--- a/public/app/plugins/panel/geomap/layers/data/geojsonLayer.ts
+++ b/public/app/plugins/panel/geomap/layers/data/geojsonLayer.ts
@@ -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 = {
create: async (map: Map, options: MapLayerOptions, 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 = {
settings: {
options: getPublicGeoJSONFiles() ?? [],
allowCustomValue: true,
+ supportVariables: true,
},
defaultValue: defaultOptions.src,
})
diff --git a/public/app/plugins/panel/geomap/utils/utils.test.ts b/public/app/plugins/panel/geomap/utils/utils.test.ts
new file mode 100644
index 00000000000..658cc4c0ed3
--- /dev/null
+++ b/public/app/plugins/panel/geomap/utils/utils.test.ts
@@ -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));
+ });
+});
diff --git a/public/app/plugins/panel/geomap/utils/utils.ts b/public/app/plugins/panel/geomap/utils/utils.ts
index 44bb31c0876..4405c317e17 100644
--- a/public/app/plugins/panel/geomap/utils/utils.ts
+++ b/public/app/plugins/panel/geomap/utils/utils.ts
@@ -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({