diff --git a/public/app/plugins/panel/geomap/GeomapPanel.tsx b/public/app/plugins/panel/geomap/GeomapPanel.tsx index 48d0634c436..7f034756160 100644 --- a/public/app/plugins/panel/geomap/GeomapPanel.tsx +++ b/public/app/plugins/panel/geomap/GeomapPanel.tsx @@ -33,7 +33,13 @@ 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, hasVariableDependencies } from './utils/utils'; +import { + updateMap, + getNewOpenLayersMap, + notifyPanelEditor, + hasVariableDependencies, + hasLayerData, +} from './utils/utils'; import { centerPointRegistry, MapCenterID } from './view'; // Allows multiple panels to share the same view instance @@ -203,6 +209,9 @@ export class GeomapPanel extends Component { this.map.setView(view); } } + + // Update legends when data changes + this.setState({ legends: this.getLegends() }); } initMapRef = async (div: HTMLDivElement) => { @@ -387,7 +396,10 @@ export class GeomapPanel extends Component { const legends: ReactNode[] = []; for (const state of this.layers) { if (state.handler.legend) { - legends.push(
{state.handler.legend}
); + const hasData = hasLayerData(state.layer); + if (hasData) { + legends.push(
{state.handler.legend}
); + } } } diff --git a/public/app/plugins/panel/geomap/utils/utils.test.ts b/public/app/plugins/panel/geomap/utils/utils.test.ts index 658cc4c0ed3..d338233d63c 100644 --- a/public/app/plugins/panel/geomap/utils/utils.test.ts +++ b/public/app/plugins/panel/geomap/utils/utils.test.ts @@ -1,14 +1,17 @@ +import Feature from 'ol/Feature'; +import Point from 'ol/geom/Point'; +import LayerGroup from 'ol/layer/Group'; +import TileLayer from 'ol/layer/Tile'; +import VectorLayer from 'ol/layer/Vector'; +import WebGLPointsLayer from 'ol/layer/WebGLPoints'; +import TileSource from 'ol/source/Tile'; +import VectorSource from 'ol/source/Vector'; + import { getTemplateSrv } from '@grafana/runtime'; // Mock the config module to avoid undefined panels error -jest.mock('app/core/config', () => ({ - config: { - panels: { - debug: { - state: 'alpha', - }, - }, - }, +jest.mock('@grafana/runtime', () => ({ + getTemplateSrv: jest.fn(), })); // Mock the dimensions module since it's imported by utils.ts @@ -24,12 +27,27 @@ jest.mock('app/plugins/datasource/grafana/datasource', () => ({ getGrafanaDatasource: jest.fn(), })); -// Mock the template service -jest.mock('@grafana/runtime', () => ({ - getTemplateSrv: jest.fn(), -})); +import { hasVariableDependencies, hasLayerData } from './utils'; -import { hasVariableDependencies } from './utils'; +// Test fixtures +const createTestFeature = () => new Feature(new Point([0, 0])); + +const createTestVectorSource = (hasFeature = false): VectorSource => { + const source = new VectorSource(); + if (hasFeature) { + source.addFeature(createTestFeature()); + } + return source; +}; + +const createTestWebGLStyle = () => ({ + symbol: { + symbolType: 'circle', + size: 8, + color: '#000000', + opacity: 1, + }, +}); describe('hasVariableDependencies', () => { beforeEach(() => { @@ -40,7 +58,6 @@ describe('hasVariableDependencies', () => { 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), @@ -99,3 +116,75 @@ describe('hasVariableDependencies', () => { expect(mockTemplateSrv.containsTemplate).toHaveBeenCalledWith(JSON.stringify(obj)); }); }); + +describe('hasLayerData', () => { + it('should return false for empty vector layer', () => { + const layer = new VectorLayer({ + source: createTestVectorSource(), + }); + expect(hasLayerData(layer)).toBe(false); + }); + + it('should return true for vector layer with features', () => { + const layer = new VectorLayer({ + source: createTestVectorSource(true), + }); + expect(hasLayerData(layer)).toBe(true); + }); + + it('should return true for layer group with data', () => { + const vectorLayer = new VectorLayer({ + source: createTestVectorSource(true), + }); + const group = new LayerGroup({ + layers: [vectorLayer], + }); + expect(hasLayerData(group)).toBe(true); + }); + + it('should return false for empty layer group', () => { + const group = new LayerGroup({ + layers: [], + }); + expect(hasLayerData(group)).toBe(false); + }); + + it('should return true for tile layer with source', () => { + const layer = new TileLayer({ + source: new TileSource({}), + }); + expect(hasLayerData(layer)).toBe(true); + }); + + it('should return false for tile layer without source', () => { + const layer = new TileLayer({}); + expect(hasLayerData(layer)).toBe(false); + }); + + it('should return true for WebGLPointsLayer with features', () => { + const layer = new WebGLPointsLayer({ + source: createTestVectorSource(true), + style: createTestWebGLStyle(), + }); + expect(hasLayerData(layer)).toBe(true); + }); + + it('should return false for empty WebGLPointsLayer', () => { + const layer = new WebGLPointsLayer({ + source: createTestVectorSource(), + style: createTestWebGLStyle(), + }); + expect(hasLayerData(layer)).toBe(false); + }); + + it('should return true for layer group with WebGLPointsLayer containing data', () => { + const webglLayer = new WebGLPointsLayer({ + source: createTestVectorSource(true), + style: createTestWebGLStyle(), + }); + const group = new LayerGroup({ + layers: [webglLayer], + }); + expect(hasLayerData(group)).toBe(true); + }); +}); diff --git a/public/app/plugins/panel/geomap/utils/utils.ts b/public/app/plugins/panel/geomap/utils/utils.ts index 4405c317e17..0e14164b413 100644 --- a/public/app/plugins/panel/geomap/utils/utils.ts +++ b/public/app/plugins/panel/geomap/utils/utils.ts @@ -1,5 +1,17 @@ import { Map as OpenLayersMap } from 'ol'; +import Geometry from 'ol/geom/Geometry'; +import Point from 'ol/geom/Point'; import { defaults as interactionDefaults } from 'ol/interaction'; +import BaseLayer from 'ol/layer/Base'; +import LayerGroup from 'ol/layer/Group'; +import ImageLayer from 'ol/layer/Image'; +import TileLayer from 'ol/layer/Tile'; +import VectorLayer from 'ol/layer/Vector'; +import VectorImage from 'ol/layer/VectorImage'; +import WebGLPointsLayer from 'ol/layer/WebGLPoints'; +import ImageSource from 'ol/source/Image'; +import TileSource from 'ol/source/Tile'; +import VectorSource from 'ol/source/Vector'; import { DataFrame, GrafanaTheme2, SelectableValue } from '@grafana/data'; import { getTemplateSrv } from '@grafana/runtime'; @@ -153,3 +165,39 @@ export const isUrl = (url: string) => { return false; } }; + +/** + * Checks if a layer has data to display + * @param layer The OpenLayers layer to check + * @returns boolean indicating if the layer has data + */ +export function hasLayerData( + layer: + | LayerGroup + | VectorLayer> + | VectorImage> + | WebGLPointsLayer> + | TileLayer + | ImageLayer + | BaseLayer +): boolean { + if (layer instanceof LayerGroup) { + return layer + .getLayers() + .getArray() + .some((subLayer) => hasLayerData(subLayer)); + } + if (layer instanceof VectorLayer || layer instanceof VectorImage) { + const source = layer.getSource(); + return source != null && source.getFeatures().length > 0; + } + if (layer instanceof WebGLPointsLayer) { + const source = layer.getSource(); + return source != null && source.getFeatures().length > 0; + } + if (layer instanceof TileLayer || layer instanceof ImageLayer) { + // For tile/image layers, check if they have a source + return Boolean(layer.getSource()); + } + return false; +}