Frontend service: Fix geomap assets not loading (#110146)

* attempting to "fix" geomap

* copy gazetteer/maps folders into dockerfile for frontend service

* add TODO comments

* remove unused import

* conditionally use public cdn path

* fix unit tests

* try refactor e2e test for better stability

* Revert "try refactor e2e test for better stability"

This reverts commit d966d68e15922613755e120f536bba5436c43d1f.

* safer

* use grafana_public_path
This commit is contained in:
Ashley Harrison
2025-08-29 16:29:57 +01:00
committed by GitHub
parent c9f815088a
commit 57db26a9bf
13 changed files with 76 additions and 34 deletions

View File

@ -3409,8 +3409,7 @@ exports[`better eslint`] = {
[0, 0, 0, "Do not use any type assertions.", "4"], [0, 0, 0, "Do not use any type assertions.", "4"],
[0, 0, 0, "Do not use any type assertions.", "5"], [0, 0, 0, "Do not use any type assertions.", "5"],
[0, 0, 0, "Do not use any type assertions.", "6"], [0, 0, 0, "Do not use any type assertions.", "6"],
[0, 0, 0, "Unexpected any. Specify a different type.", "7"], [0, 0, 0, "Unexpected any. Specify a different type.", "7"]
[0, 0, 0, "Unexpected any. Specify a different type.", "8"]
], ],
"public/app/plugins/datasource/graphite/configuration/ConfigEditor.tsx:5381": [ "public/app/plugins/datasource/graphite/configuration/ConfigEditor.tsx:5381": [
[0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"], [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"],

View File

@ -93,6 +93,8 @@ docker_build('grafana-fs-dev',
'public/dashboards', 'public/dashboards',
'public/app/plugins', 'public/app/plugins',
'public/build/assets-manifest.json', 'public/build/assets-manifest.json',
'public/gazetteer',
'public/maps',
], ],
# Sync paths are relative to the Tiltfile # Sync paths are relative to the Tiltfile

View File

@ -22,9 +22,11 @@ COPY public/emails public/emails
COPY public/views public/views COPY public/views public/views
COPY public/dashboards public/dashboards COPY public/dashboards public/dashboards
COPY public/app/plugins public/app/plugins COPY public/app/plugins public/app/plugins
COPY public/gazetteer public/gazetteer
COPY public/maps public/maps
ADD devenv/frontend-service/build/grafana bin/grafana ADD devenv/frontend-service/build/grafana bin/grafana
COPY public/build/assets-manifest.json public/build/assets-manifest.json COPY public/build/assets-manifest.json public/build/assets-manifest.json
ENTRYPOINT ["bin/grafana", "server"] ENTRYPOINT ["bin/grafana", "server"]

View File

@ -2,8 +2,6 @@ import { getCenterPointWGS84 } from 'app/features/transformers/spatial/utils';
import { getGazetteer } from './gazetteer'; import { getGazetteer } from './gazetteer';
let backendResults: Record<string, unknown> = { hello: 'world' };
const geojsonObject = { const geojsonObject = {
type: 'FeatureCollection', type: 'FeatureCollection',
features: [ features: [
@ -43,20 +41,16 @@ const geojsonObject = {
], ],
}; };
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
getBackendSrv: () => ({
get: jest.fn().mockResolvedValue(backendResults),
}),
}));
describe('Placename lookup from geojson format', () => { describe('Placename lookup from geojson format', () => {
beforeEach(() => { beforeEach(() => {
backendResults = { hello: 'world' }; jest.spyOn(global, 'fetch').mockResolvedValue({
ok: true,
status: 200,
json: jest.fn().mockResolvedValue(geojsonObject),
} as unknown as Response);
}); });
it('can lookup by id', async () => { it('can lookup by id', async () => {
backendResults = geojsonObject;
const gaz = await getGazetteer('local'); const gaz = await getGazetteer('local');
expect(gaz.error).toBeUndefined(); expect(gaz.error).toBeUndefined();
expect(getCenterPointWGS84(gaz.find('A')?.geometry())).toMatchInlineSnapshot(` expect(getCenterPointWGS84(gaz.find('A')?.geometry())).toMatchInlineSnapshot(`
@ -67,7 +61,6 @@ describe('Placename lookup from geojson format', () => {
`); `);
}); });
it('can look up by a code', async () => { it('can look up by a code', async () => {
backendResults = geojsonObject;
const gaz = await getGazetteer('airports'); const gaz = await getGazetteer('airports');
expect(gaz.error).toBeUndefined(); expect(gaz.error).toBeUndefined();
expect(getCenterPointWGS84(gaz.find('B')?.geometry())).toMatchInlineSnapshot(` expect(getCenterPointWGS84(gaz.find('B')?.geometry())).toMatchInlineSnapshot(`
@ -79,7 +72,6 @@ describe('Placename lookup from geojson format', () => {
}); });
it('can look up by an id property', async () => { it('can look up by an id property', async () => {
backendResults = geojsonObject;
const gaz = await getGazetteer('airports'); const gaz = await getGazetteer('airports');
expect(gaz.error).toBeUndefined(); expect(gaz.error).toBeUndefined();
expect(getCenterPointWGS84(gaz.find('C')?.geometry())).toMatchInlineSnapshot(` expect(getCenterPointWGS84(gaz.find('C')?.geometry())).toMatchInlineSnapshot(`

View File

@ -2,7 +2,6 @@ import { getCenter } from 'ol/extent';
import { Geometry, Point } from 'ol/geom'; import { Geometry, Point } from 'ol/geom';
import { DataFrame, Field, FieldType, KeyValue, toDataFrame } from '@grafana/data'; import { DataFrame, Field, FieldType, KeyValue, toDataFrame } from '@grafana/data';
import { getBackendSrv } from '@grafana/runtime';
import { frameFromGeoJSON } from '../format/geojson'; import { frameFromGeoJSON } from '../format/geojson';
import { pointFieldFromLonLat, pointFieldFromGeohash } from '../format/utils'; import { pointFieldFromLonLat, pointFieldFromGeohash } from '../format/utils';
@ -181,7 +180,7 @@ export function frameAsGazetter(frame: DataFrame, opts: { path: string; keys?: s
const registry: KeyValue<Gazetteer> = {}; const registry: KeyValue<Gazetteer> = {};
export const COUNTRIES_GAZETTEER_PATH = 'public/gazetteer/countries.json'; export const COUNTRIES_GAZETTEER_PATH = `${window.__grafana_public_path__}build/gazetteer/countries.json`;
/** /**
* Given a path to a file return a cached lookup function * Given a path to a file return a cached lookup function
@ -196,7 +195,8 @@ export async function getGazetteer(path?: string): Promise<Gazetteer> {
if (!lookup) { if (!lookup) {
try { try {
// block the async function // block the async function
const data = await getBackendSrv().get(path!); const response = await fetch(path);
const data = await response.json();
lookup = loadGazetteer(path, data); lookup = loadGazetteer(path, data);
} catch (err) { } catch (err) {
console.warn('Error loading placename lookup', path, err); console.warn('Error loading placename lookup', path, err);

View File

@ -4,22 +4,18 @@ import countriesJSON from '../../../../gazetteer/countries.json';
import { getGazetteer } from './gazetteer'; import { getGazetteer } from './gazetteer';
let backendResults: Record<string, string> | Array<Record<string, unknown>> = { hello: 'world' }; const backendResults: Record<string, string> | Array<Record<string, unknown>> = countriesJSON;
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
getBackendSrv: () => ({
get: jest.fn().mockResolvedValue(backendResults),
}),
}));
describe('Placename lookup from worldmap format', () => { describe('Placename lookup from worldmap format', () => {
beforeEach(() => { beforeEach(() => {
backendResults = { hello: 'world' }; jest.spyOn(global, 'fetch').mockResolvedValue({
ok: true,
status: 200,
json: jest.fn().mockResolvedValue(backendResults),
} as unknown as Response);
}); });
it('unified worldmap config', async () => { it('unified worldmap config', async () => {
backendResults = countriesJSON;
const gaz = await getGazetteer('countries'); const gaz = await getGazetteer('countries');
expect(gaz.error).toBeUndefined(); expect(gaz.error).toBeUndefined();
expect(toLonLat(gaz.find('US')?.point()?.getCoordinates()!)).toMatchInlineSnapshot(` expect(toLonLat(gaz.find('US')?.point()?.getCoordinates()!)).toMatchInlineSnapshot(`

View File

@ -10,10 +10,53 @@ const longitude = [0, -74.1];
const latitude = [0, 40.7]; const latitude = [0, 40.7];
const geohash = ['9q94r', 'dr5rs']; const geohash = ['9q94r', 'dr5rs'];
const names = ['A', 'B']; const names = ['A', 'B'];
const geojsonObject = {
type: 'FeatureCollection',
features: [
{
id: 'A',
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [0, 0],
},
properties: {
hello: 'A',
},
},
{
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [1, 1],
},
properties: {
some_code: 'B',
hello: 'B',
},
},
{
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [2, 2],
},
properties: {
an_id: 'C',
hello: 'C',
},
},
],
};
describe('handle location parsing', () => { describe('handle location parsing', () => {
beforeEach(() => { beforeEach(() => {
jest.spyOn(console, 'warn').mockImplementation(); jest.spyOn(console, 'warn').mockImplementation();
jest.spyOn(global, 'fetch').mockResolvedValue({
ok: true,
status: 200,
json: jest.fn().mockResolvedValue(geojsonObject),
} as unknown as Response);
}); });
it('auto should find geohash field', async () => { it('auto should find geohash field', async () => {

View File

@ -180,7 +180,7 @@ export class GrafanaDatasource extends DataSourceWithBackend<GrafanaQuery> {
}, },
], ],
maxDataPoints, maxDataPoints,
} as any).pipe( } as DataQueryRequest<GrafanaQuery>).pipe(
map((v) => { map((v) => {
const frame = v.data[0] ?? new MutableDataFrame(); const frame = v.data[0] ?? new MutableDataFrame();
return new DataFrameView<FileElement>(frame); return new DataFrameView<FileElement>(frame);

View File

@ -394,7 +394,7 @@ export class GeomapPanel extends Component<Props, State> {
); );
} }
this.mouseWheelZoom!.setActive(Boolean(options.mouseWheelZoom)); this.mouseWheelZoom?.setActive(Boolean(options.mouseWheelZoom));
if (options.showAttribution) { if (options.showAttribution) {
this.map.addControl(new Attribution({ collapsed: true, collapsible: true })); this.map.addControl(new Attribution({ collapsed: true, collapsible: true }));

View File

@ -80,7 +80,7 @@ export const geojsonLayer: MapLayerRegistryItem<GeoJSONMapperConfig> = {
const interpolatedUrl = getTemplateSrv().replace(config.src || ''); const interpolatedUrl = getTemplateSrv().replace(config.src || '');
const source = new VectorSource({ const source = new VectorSource({
url: interpolatedUrl, url: `${window.__grafana_public_path__}build/${interpolatedUrl.replace(/^(public\/)/, '')}`,
format: new GeoJSON(), format: new GeoJSON(),
}); });

View File

@ -76,7 +76,7 @@ describe('Worldmap Migrations', () => {
"min": 2, "min": 2,
}, },
"symbol": { "symbol": {
"fixed": "img/icons/marker/circle.svg", "fixed": "build/img/icons/marker/circle.svg",
"mode": "fixed", "mode": "fixed",
}, },
"symbolAlign": { "symbolAlign": {

View File

@ -74,7 +74,7 @@ export const defaultStyleConfig = Object.freeze({
opacity: 0.4, opacity: 0.4,
symbol: { symbol: {
mode: ResourceDimensionMode.Fixed, mode: ResourceDimensionMode.Fixed,
fixed: 'img/icons/marker/circle.svg', fixed: 'build/img/icons/marker/circle.svg',
}, },
symbolAlign: { symbolAlign: {
horizontal: HorizontalAlign.Center, horizontal: HorizontalAlign.Center,

View File

@ -75,6 +75,14 @@ module.exports = {
from: 'public/img', from: 'public/img',
to: 'img', to: 'img',
}, },
{
from: 'public/maps',
to: 'maps',
},
{
from: 'public/gazetteer',
to: 'gazetteer',
},
], ],
}), }),
], ],