Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions components/AlertsDashboard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import type {
AllowedFileExtensions,
Basemap,
BasemapConfig,
MapboxStyleConfig,
MapLegendItem,
} from "@/types";
import type { Feature, FeatureCollection } from "geojson";
Expand Down Expand Up @@ -97,7 +98,7 @@ const props = defineProps<{
mapboxLongitude: number;
mapboxPitch: number | null;
mapboxProjection: string;
mapboxStyle: string;
mapboxStyle: MapboxStyleConfig;
mapboxBasemaps?: BasemapConfig[];
mapboxZoom: number;
mapbox3d: boolean;
Expand All @@ -119,6 +120,7 @@ const calculateHectares = ref(false);
const dateOptions = ref();
const hasRulerControl = ref(false);
const map = ref();
const mapReady = ref(false);
const showBasemapSelector = ref(false);
const showIntroPanel = ref(true);
const showSidebar = ref(true);
Expand Down Expand Up @@ -762,7 +764,10 @@ const addAlertsData = async () => {
}

// Add cluster count label layer
if (!map.value.getLayer(`${layerId}-cluster-count`)) {
if (
map.value.getStyle().glyphs &&
!map.value.getLayer(`${layerId}-cluster-count`)
) {
map.value.addLayer({
id: `${layerId}-cluster-count`,
type: "symbol",
Expand Down Expand Up @@ -902,7 +907,10 @@ const addAlertsData = async () => {
}

// Add cluster count label
if (!map.value.getLayer(`${layerId}-cluster-count`)) {
if (
map.value.getStyle().glyphs &&
!map.value.getLayer(`${layerId}-cluster-count`)
) {
map.value.addLayer({
id: `${layerId}-cluster-count`,
type: "symbol",
Expand Down Expand Up @@ -1409,6 +1417,8 @@ const prepareMapLegendContent = () => {
}

mapLegendContent.value = legendItems;
// E2E tests wait for this after the idle-gated legend content is ready.
mapReady.value = true;
});
};

Expand Down Expand Up @@ -1705,7 +1715,7 @@ onBeforeUnmount(() => {

<template>
<div>
<div id="map"></div>
<div id="map" :data-map-ready="mapReady || undefined"></div>
<button
v-if="!showSidebar"
class="absolute top-2.5 left-2.5 z-10 bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline mx-2"
Expand Down
3 changes: 2 additions & 1 deletion components/MapView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import type {
DataEntry,
Dataset,
FilterValues,
MapboxStyleConfig,
MapLegendItem,
MapStatistics,
} from "@/types";
Expand All @@ -54,7 +55,7 @@ const props = defineProps<{
mapboxLongitude: number;
mapboxPitch: number | null;
mapboxProjection: string;
mapboxStyle: string;
mapboxStyle: MapboxStyleConfig;
mapboxBasemaps?: BasemapConfig[];
mapboxZoom: number;
mapbox3d: boolean;
Expand Down
5 changes: 3 additions & 2 deletions components/shared/BasemapSelector.vue
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
<script setup lang="ts">
import Datepicker from "vue-datepicker-next";
import "vue-datepicker-next/index.css";
import type { PropType } from "vue";

import type { Basemap, BasemapConfig } from "@/types";
import type { Basemap, BasemapConfig, MapboxStyleConfig } from "@/types";
import { Layers } from "lucide-vue-next";

const props = defineProps({
hasRulerControl: Boolean,
mapboxStyle: String,
mapboxStyle: [String, Object] as PropType<MapboxStyleConfig>,
mapboxBasemaps: {
type: Array as () => BasemapConfig[],
default: () => [],
Expand Down
12 changes: 6 additions & 6 deletions server/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import type { BasemapConfig, ViewConfig } from "@/types";
import type { BasemapConfig, MapboxStyleConfig, ViewConfig } from "@/types";

import { fetchTableNames } from "@/server/database/dbOperations";

export type ParsedBasemaps = {
basemaps: BasemapConfig[];
defaultMapboxStyle?: string;
defaultMapboxStyle?: MapboxStyleConfig;
};

/**
* Parse basemaps configuration from ViewConfig for a given table, with legacy fallback.
*/
export const parseBasemaps = (tableConfig: ViewConfig): ParsedBasemaps => {
let basemaps: BasemapConfig[] = [];
let defaultMapboxStyle: string | undefined;
let defaultMapboxStyle: MapboxStyleConfig | undefined;

if (tableConfig.MAPBOX_BASEMAPS) {
try {
Expand All @@ -25,7 +25,7 @@ export const parseBasemaps = (tableConfig: ViewConfig): ParsedBasemaps => {
}
} catch {
// If parsing fails, fall back to legacy MAPBOX_STYLE
defaultMapboxStyle = tableConfig.MAPBOX_STYLE as string | undefined;
defaultMapboxStyle = tableConfig.MAPBOX_STYLE;
if (defaultMapboxStyle) {
basemaps = [
{
Expand All @@ -38,11 +38,11 @@ export const parseBasemaps = (tableConfig: ViewConfig): ParsedBasemaps => {
}
} else if (tableConfig.MAPBOX_STYLE) {
// Legacy fallback
defaultMapboxStyle = tableConfig.MAPBOX_STYLE as string | undefined;
defaultMapboxStyle = tableConfig.MAPBOX_STYLE;
basemaps = [
{
name: "Default Style",
style: defaultMapboxStyle as string,
style: defaultMapboxStyle,
isDefault: true,
},
];
Expand Down
12 changes: 7 additions & 5 deletions tests/db-seed/guardianconnector.sql
Original file line number Diff line number Diff line change
Expand Up @@ -4,40 +4,42 @@
INSERT INTO public_views (table_name) VALUES ('seed_survey_data') ON CONFLICT DO NOTHING;
INSERT INTO public_views (table_name) VALUES ('fake_alerts') ON CONFLICT DO NOTHING;

-- MAPBOX_STYLE uses a minimal local style object so E2E map load does not depend on
-- remote Mapbox style API availability in CI.
INSERT INTO views (view_name, view_type, primary_dataset, secondary_dataset, view_config) VALUES
(
'seed_survey_data',
'gallery',
'seed_survey_data',
NULL,
'{"MAPBOX_STYLE":"mapbox://styles/mapbox/streets-v12","MAPBOX_ACCESS_TOKEN":"{MAPBOX_ACCESS_TOKEN}","MAPBOX_ZOOM":16,"MAPBOX_CENTER_LATITUDE":"3.44704","MAPBOX_CENTER_LONGITUDE":"-76.53995","MAPBOX_PROJECTION":"globe","MAPBOX_BEARING":0,"MAPBOX_PITCH":0,"FRONT_END_FILTER_COLUMN":"community","MEDIA_BASE_PATH":"{MEDIA_BASE_PATH}","ROUTE_LEVEL_PERMISSION":"anyone"}'
'{"MAPBOX_STYLE":{"version":8,"sources":{},"layers":[{"id":"background","type":"background","paint":{"background-color":"#f8fafc"}}]},"MAPBOX_ACCESS_TOKEN":"{MAPBOX_ACCESS_TOKEN}","MAPBOX_ZOOM":16,"MAPBOX_CENTER_LATITUDE":"3.44704","MAPBOX_CENTER_LONGITUDE":"-76.53995","MAPBOX_PROJECTION":"globe","MAPBOX_BEARING":0,"MAPBOX_PITCH":0,"FRONT_END_FILTER_COLUMN":"community","MEDIA_BASE_PATH":"{MEDIA_BASE_PATH}","ROUTE_LEVEL_PERMISSION":"anyone"}'
),
(
'bcmform_responses',
'map',
'bcmform_responses',
NULL,
'{"MAPBOX_STYLE":"mapbox://styles/mapbox/streets-v12","MAPBOX_ACCESS_TOKEN":"{MAPBOX_ACCESS_TOKEN}","MAPBOX_ZOOM":16,"MAPBOX_CENTER_LATITUDE":"3.44704","MAPBOX_CENTER_LONGITUDE":"-76.53995","MAPBOX_PROJECTION":"globe","MAPBOX_BEARING":0,"MAPBOX_PITCH":0,"FRONT_END_FILTER_COLUMN":"community","MEDIA_BASE_PATH":"{MEDIA_BASE_PATH}","ROUTE_LEVEL_PERMISSION":"member"}'
'{"MAPBOX_STYLE":{"version":8,"sources":{},"layers":[{"id":"background","type":"background","paint":{"background-color":"#f8fafc"}}]},"MAPBOX_ACCESS_TOKEN":"{MAPBOX_ACCESS_TOKEN}","MAPBOX_ZOOM":16,"MAPBOX_CENTER_LATITUDE":"3.44704","MAPBOX_CENTER_LONGITUDE":"-76.53995","MAPBOX_PROJECTION":"globe","MAPBOX_BEARING":0,"MAPBOX_PITCH":0,"FRONT_END_FILTER_COLUMN":"community","MEDIA_BASE_PATH":"{MEDIA_BASE_PATH}","ROUTE_LEVEL_PERMISSION":"member"}'
),
(
'bcmform_responses',
'gallery',
'bcmform_responses',
NULL,
'{"MAPBOX_STYLE":"mapbox://styles/mapbox/streets-v12","MAPBOX_ACCESS_TOKEN":"{MAPBOX_ACCESS_TOKEN}","MAPBOX_ZOOM":16,"MAPBOX_CENTER_LATITUDE":"3.44704","MAPBOX_CENTER_LONGITUDE":"-76.53995","MAPBOX_PROJECTION":"globe","MAPBOX_BEARING":0,"MAPBOX_PITCH":0,"FRONT_END_FILTER_COLUMN":"community","MEDIA_BASE_PATH":"{MEDIA_BASE_PATH}","ROUTE_LEVEL_PERMISSION":"member"}'
'{"MAPBOX_STYLE":{"version":8,"sources":{},"layers":[{"id":"background","type":"background","paint":{"background-color":"#f8fafc"}}]},"MAPBOX_ACCESS_TOKEN":"{MAPBOX_ACCESS_TOKEN}","MAPBOX_ZOOM":16,"MAPBOX_CENTER_LATITUDE":"3.44704","MAPBOX_CENTER_LONGITUDE":"-76.53995","MAPBOX_PROJECTION":"globe","MAPBOX_BEARING":0,"MAPBOX_PITCH":0,"FRONT_END_FILTER_COLUMN":"community","MEDIA_BASE_PATH":"{MEDIA_BASE_PATH}","ROUTE_LEVEL_PERMISSION":"member"}'
),
(
'fake_alerts',
'alerts',
'fake_alerts',
'mapeo_data',
'{"EMBED_MEDIA":"YES","MEDIA_BASE_PATH_ALERTS":"","MEDIA_BASE_PATH":"","LOGO_URL":"https://conservationmetrics.com/wp-content/themes/conservation-metrics/images/logo-conservation-metrics.png","MAPBOX_STYLE":"mapbox://styles/mapbox/satellite-streets-v12","MAPBOX_PROJECTION":"globe","MAPBOX_CENTER_LATITUDE":"38","MAPBOX_CENTER_LONGITUDE":"-79","MAPBOX_ZOOM":7,"MAPBOX_PITCH":0,"MAPBOX_BEARING":0,"MAPBOX_3D":false,"MAPEO_CATEGORY_IDS":"threat","MAP_LEGEND_LAYER_IDS":"road-primary,aerialway","ALERT_RESOURCES":"NO","MAPBOX_ACCESS_TOKEN":"{MAPBOX_ACCESS_TOKEN}","PLANET_API_KEY":"{PLANET_API_KEY}","ROUTE_LEVEL_PERMISSION":"anyone"}'
'{"EMBED_MEDIA":"YES","MEDIA_BASE_PATH_ALERTS":"","MEDIA_BASE_PATH":"","LOGO_URL":"https://conservationmetrics.com/wp-content/themes/conservation-metrics/images/logo-conservation-metrics.png","MAPBOX_STYLE":{"version":8,"sources":{},"layers":[{"id":"background","type":"background","paint":{"background-color":"#f8fafc"}}]},"MAPBOX_PROJECTION":"globe","MAPBOX_CENTER_LATITUDE":"38","MAPBOX_CENTER_LONGITUDE":"-79","MAPBOX_ZOOM":7,"MAPBOX_PITCH":0,"MAPBOX_BEARING":0,"MAPBOX_3D":false,"MAPEO_CATEGORY_IDS":"threat","MAP_LEGEND_LAYER_IDS":"road-primary,aerialway","ALERT_RESOURCES":"NO","MAPBOX_ACCESS_TOKEN":"{MAPBOX_ACCESS_TOKEN}","PLANET_API_KEY":"{PLANET_API_KEY}","ROUTE_LEVEL_PERMISSION":"anyone"}'
),
(
'gfw_alerts_viirs',
'alerts',
'gfw_alerts_viirs',
'mapeo_data',
'{"EMBED_MEDIA":"NO","MEDIA_BASE_PATH_ALERTS":"","MEDIA_BASE_PATH":"","MAPBOX_STYLE":"mapbox://styles/mapbox/satellite-streets-v12","MAPBOX_PROJECTION":"globe","MAPBOX_CENTER_LATITUDE":"1.20","MAPBOX_CENTER_LONGITUDE":"34.60","MAPBOX_ZOOM":8,"MAPBOX_PITCH":0,"MAPBOX_BEARING":0,"MAPBOX_3D":false,"MAPEO_CATEGORY_IDS":"threat","MAP_LEGEND_LAYER_IDS":"road-primary,aerialway","ALERT_RESOURCES":"NO","MAPBOX_ACCESS_TOKEN":"{MAPBOX_ACCESS_TOKEN}","PLANET_API_KEY":"{PLANET_API_KEY}","ROUTE_LEVEL_PERMISSION":"anyone"}'
'{"EMBED_MEDIA":"NO","MEDIA_BASE_PATH_ALERTS":"","MEDIA_BASE_PATH":"","MAPBOX_STYLE":{"version":8,"sources":{},"layers":[{"id":"background","type":"background","paint":{"background-color":"#f8fafc"}}]},"MAPBOX_PROJECTION":"globe","MAPBOX_CENTER_LATITUDE":"1.20","MAPBOX_CENTER_LONGITUDE":"34.60","MAPBOX_ZOOM":8,"MAPBOX_PITCH":0,"MAPBOX_BEARING":0,"MAPBOX_3D":false,"MAPEO_CATEGORY_IDS":"threat","MAP_LEGEND_LAYER_IDS":"road-primary,aerialway","ALERT_RESOURCES":"NO","MAPBOX_ACCESS_TOKEN":"{MAPBOX_ACCESS_TOKEN}","PLANET_API_KEY":"{PLANET_API_KEY}","ROUTE_LEVEL_PERMISSION":"anyone"}'
)
ON CONFLICT (view_type, primary_dataset) DO NOTHING;
62 changes: 8 additions & 54 deletions tests/e2e/01-alerts.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,29 +85,8 @@ test("alerts dashboard - layer visibility toggles", async ({
const mapCanvas = page.locator("canvas.mapboxgl-canvas").first();
await expect(mapCanvas).toBeVisible();

// 8. Wait for the map to be fully loaded
await page.waitForFunction(
() => {
// @ts-expect-error _testMap is exposed for E2E testing only
const map = window._testMap;
return map?.isStyleLoaded() && map.loaded();
},
{ timeout: 5000 },
);

// 9. Wait for the map legend to be prepared and visible
// First wait for the map to be idle (which triggers legend preparation)
await page.waitForFunction(
() => {
// @ts-expect-error _testMap is exposed for E2E testing only
const map = window._testMap;
return map && !map.isMoving() && map.loaded();
},
{ timeout: 10000 },
);

// Wait a bit more for the legend to be prepared
await page.waitForTimeout(2000);
// 8. Wait for the map to be fully loaded and ready (layers added, legend populated)
await page.locator("#map[data-map-ready='true']").waitFor();

const mapLegend = page.getByTestId("map-legend");

Expand Down Expand Up @@ -423,15 +402,8 @@ test("alerts dashboard - LineString buffer click behavior", async ({
const mapCanvas = page.locator("canvas.mapboxgl-canvas").first();
await expect(mapCanvas).toBeVisible();

// 8. Wait for the map to be fully loaded
await page.waitForFunction(
() => {
// @ts-expect-error _testMap is exposed for E2E testing only
const map = window._testMap;
return map?.isStyleLoaded() && map.loaded();
},
{ timeout: 5000 },
);
// 8. Wait for the map to be fully loaded and ready
await page.locator("#map[data-map-ready='true']").waitFor();

// 9. Check if LineString features exist
const hasLineStrings = await page.evaluate(() => {
Expand Down Expand Up @@ -570,15 +542,8 @@ test("alerts dashboard - geometry type specific interactions", async ({
const mapCanvas = page.locator("canvas.mapboxgl-canvas").first();
await expect(mapCanvas).toBeVisible();

// 8. Wait for the map to be fully loaded
await page.waitForFunction(
() => {
// @ts-expect-error _testMap is exposed for E2E testing only
const map = window._testMap;
return map?.isStyleLoaded() && map.loaded();
},
{ timeout: 5000 },
);
// 8. Wait for the map to be fully loaded and ready
await page.locator("#map[data-map-ready='true']").waitFor();

// 9. Test Point features (clustered circles)
const pointFeatures = await page.evaluate(() => {
Expand Down Expand Up @@ -786,19 +751,8 @@ test("alerts dashboard - cluster circles and centroid selection behavior", async
const mapCanvas = page.locator("canvas.mapboxgl-canvas").first();
await expect(mapCanvas).toBeVisible();

await page.waitForFunction(() => {
// @ts-expect-error _testMap is exposed for E2E testing only
return !!window._testMap;
});

await page.waitForFunction(
() => {
// @ts-expect-error _testMap is exposed for E2E testing only
const map = window._testMap;
return map?.isStyleLoaded() && map.loaded();
},
{ timeout: 5000 },
);
// Wait for the map to be fully loaded and ready
await page.locator("#map[data-map-ready='true']").waitFor();

// Test 1: Verify cluster circles exist (instead of old symbols)
const clusterFeatures = await page.evaluate(() => {
Expand Down
Loading
Loading