From 3aef8f83317fd36d72de82ebeaff891b7c5bccbc Mon Sep 17 00:00:00 2001 From: Drew Slobodnjak <60050885+drew08t@users.noreply.github.com> Date: Thu, 12 Jun 2025 13:20:06 -0700 Subject: [PATCH] [release-11.6.3] Geomap: Require layer to have data for legend (#106228) * Geomap: Require layer to have data for legend (#105580) (cherry picked from commit 16a6d61ca782eb68f643f8ccc6fba165cf64a009) * Remove test for non-existent function * Sync up go work sum * Remove unused import --- .../app/plugins/panel/geomap/GeomapPanel.tsx | 10 +- .../plugins/panel/geomap/utils/utils.test.ts | 120 ++++++++++++++++++ .../app/plugins/panel/geomap/utils/utils.ts | 48 +++++++ 3 files changed, 176 insertions(+), 2 deletions(-) create mode 100644 public/app/plugins/panel/geomap/utils/utils.test.ts diff --git a/public/app/plugins/panel/geomap/GeomapPanel.tsx b/public/app/plugins/panel/geomap/GeomapPanel.tsx index bbbb0eb3510..5dd98bf97c9 100644 --- a/public/app/plugins/panel/geomap/GeomapPanel.tsx +++ b/public/app/plugins/panel/geomap/GeomapPanel.tsx @@ -31,7 +31,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, hasLayerData } from './utils/utils'; import { centerPointRegistry, MapCenterID } from './view'; // Allows multiple panels to share the same view instance @@ -182,6 +182,9 @@ export class GeomapPanel extends Component { this.map.setView(view); } } + + // Update legends when data changes + this.setState({ legends: this.getLegends() }); } initMapRef = async (div: HTMLDivElement) => { @@ -366,7 +369,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 new file mode 100644 index 00000000000..c6ba8f27d05 --- /dev/null +++ b/public/app/plugins/panel/geomap/utils/utils.test.ts @@ -0,0 +1,120 @@ +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'; + +// Mock the config module to avoid undefined panels error +jest.mock('@grafana/runtime', () => ({ + getTemplateSrv: jest.fn(), +})); + +// 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(), +})); + +import { hasLayerData } 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('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 6587dda7ee2..a758487b950 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 { SelectableValue } from '@grafana/data'; import { DataFrame, GrafanaTheme2 } from '@grafana/data/src'; @@ -144,3 +156,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; +}