diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ddee6d8bda9..580682f04789 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Next Next Release +#### Enso IDE + +- [ENSO_IDE_MAPBOX_API_TOKEN environment variable should be provided to enable + GeoMap visualization][11889]. +- [Round ‘Add component’ button under the component menu replaced by a small + button protruding from the output port.][11836]. + +[11889]: https://github.com/enso-org/enso/pull/11889 +[11836]: https://github.com/enso-org/enso/pull/11836 + #### Enso Language & Runtime - [Intersection types & type checks][11600] diff --git a/app/gui/env.d.ts b/app/gui/env.d.ts index f7095e04d189..2babbb394e40 100644 --- a/app/gui/env.d.ts +++ b/app/gui/env.d.ts @@ -175,6 +175,7 @@ declare global { readonly projectManagementApi?: ProjectManagementApi readonly fileBrowserApi?: FileBrowserApi readonly versionInfo?: VersionInfo + readonly mapBoxApiToken?: () => string toggleDevtools: () => void /** * If set to `true`, animations will be disabled. diff --git a/app/gui/package.json b/app/gui/package.json index fab596327b6e..a9df2bc70666 100644 --- a/app/gui/package.json +++ b/app/gui/package.json @@ -178,7 +178,7 @@ "@types/d3": "^7.4.0", "@types/hash-sum": "^1.0.0", "@types/jsdom": "^21.1.1", - "@types/mapbox-gl": "^2.7.13", + "@types/mapbox-gl": "^3.4.1", "@types/shuffle-seed": "^1.1.0", "@types/tar": "^6.1.4", "@types/wicg-file-system-access": "^2023.10.2", diff --git a/app/gui/src/project-view/components/visualizations/GeoMapVisualization.vue b/app/gui/src/project-view/components/visualizations/GeoMapVisualization.vue index 47c9f012781c..020064b374b5 100644 --- a/app/gui/src/project-view/components/visualizations/GeoMapVisualization.vue +++ b/app/gui/src/project-view/components/visualizations/GeoMapVisualization.vue @@ -8,10 +8,11 @@ export const defaultPreprocessor = [ ] as const export const scripts = [ // mapbox-gl does not have an ESM release. - 'https://api.tiles.mapbox.com/mapbox-gl-js/v2.15.0/mapbox-gl.js', + 'https://api.tiles.mapbox.com/mapbox-gl-js/v3.8.0/mapbox-gl.js', // The deck.gl scripting API is not available in the ESM module. - 'https://cdn.jsdelivr.net/npm/deck.gl@8.9.27/dist.min.js', + 'https://cdn.jsdelivr.net/npm/deck.gl@9.0.38/dist.min.js', ] +export const styles = ['https://api.tiles.mapbox.com/mapbox-gl-js/v3.8.0/mapbox-gl.css'] /** * Provides a mapbox & deck.gl-based map visualization. @@ -100,8 +101,8 @@ const props = defineProps<{ data: Data }>() * Mapbox API access token. * All the limits of API are listed here: https://docs.mapbox.com/api/#rate-limits */ -const TOKEN = import.meta.env.ENSO_IDE_MAPBOX_API_TOKEN -if (TOKEN == null) { +const TOKEN = window.mapBoxApiToken?.() +if (!TOKEN) { console.warn( 'Mapbox API token is missing, to use Geo Map visualization please provide ENSO_IDE_MAPBOX_API_TOKEN.', ) @@ -170,9 +171,15 @@ function updateState(data: Data) { } const center = centerPoint() + function safeOrDefault(value: number | undefined, defaultValue: number): number { + if (value === 0 || value == null || Number.isNaN(value)) return defaultValue + return value + } + const width = safeOrDefault(mapNode.value?.clientWidth, 600) + const height = safeOrDefault(mapNode.value?.clientHeight, 400) const viewPort = new deck.WebMercatorViewport({ - width: mapNode.value?.clientWidth ?? 600, - height: mapNode.value?.clientHeight ?? 400, + width, + height, longitude: center.longitude, latitude: center.latitude, zoom: DEFAULT_MAP_ZOOM, @@ -229,6 +236,16 @@ function initDeckGl() { initialViewState: viewState.value, controller: controller.value, }) as any + // Hotfix for https://github.com/mapbox/mapbox-gl-js/issues/13355 + ;(deckgl.value as any)._map.map._updateContainerDimensions = function () { + if (!this._container) return + + const width = this._container.offsetWidth || 400 + const height = this._container.offsetHeight || 300 + + this._containerWidth = width + this._containerHeight = height + } } catch (error) { console.error(error) resetState() diff --git a/app/gui/src/project-view/stores/visualization/index.ts b/app/gui/src/project-view/stores/visualization/index.ts index 4a32b0d4eeba..914d270b675a 100644 --- a/app/gui/src/project-view/stores/visualization/index.ts +++ b/app/gui/src/project-view/stores/visualization/index.ts @@ -22,6 +22,7 @@ import { type VisualizationId, } from '@/stores/visualization/metadata' import type { VisualizationModule } from '@/stores/visualization/runtimeTypes' +import { assert } from '@/util/assert' import type { Opt } from '@/util/data/opt' import { isUrlString } from '@/util/data/urlString' import { isIconName } from '@/util/iconName' @@ -103,40 +104,47 @@ export const [provideVisualizationStore, useVisualizationStore] = createContextS }) } + const loadedScripts = new Set() + const loadedStyles = new Set() const scriptsNode = document.head.appendChild(document.createElement('div')) scriptsNode.classList.add('visualization-scripts') - const loadedScripts = new Set() - function loadScripts(module: VisualizationModule) { + + function loadAsync( + urls: string[], + type: 'scripts' | 'styles', + container: HTMLElement, + loaded: Set, + ) { const promises: Promise[] = [] - if ('scripts' in module && module.scripts) { - if (!Array.isArray(module.scripts)) { - console.warn('Visualiation scripts should be an array:', module.scripts) - } - const scripts = Array.isArray(module.scripts) ? module.scripts : [module.scripts] - for (const url of scripts) { - if (typeof url !== 'string') { - console.warn('Visualization script should be a string, skipping URL:', url) - } else if (!loadedScripts.has(url)) { - loadedScripts.add(url) - const node = document.createElement('script') + for (const url of urls) { + if (!loaded.has(url)) { + loaded.add(url) + const nodeKind = type === 'scripts' ? 'script' : 'link' + const node = document.createElement(nodeKind) + if (type === 'styles') { + assert(node instanceof HTMLLinkElement) + node.href = url + node.rel = 'stylesheet' + } else if (type === 'scripts') { + assert(node instanceof HTMLScriptElement) node.src = url - // Some resources still set only "Access-Control-Allow-Origin" in the response. - // We need to explicitly make a request CORS - see https://resourcepolicy.fyi - node.crossOrigin = 'anonymous' - promises.push( - new Promise((resolve, reject) => { - node.addEventListener('load', () => { - resolve() - node.remove() - }) - node.addEventListener('error', () => { - reject() - node.remove() - }) - }), - ) - scriptsNode.appendChild(node) } + // Some resources still set only "Access-Control-Allow-Origin" in the response. + // We need to explicitly make a request CORS - see https://resourcepolicy.fyi + node.crossOrigin = 'anonymous' + promises.push( + new Promise((resolve, reject) => { + node.addEventListener('load', () => { + resolve() + node.remove() + }) + node.addEventListener('error', () => { + reject() + node.remove() + }) + }), + ) + container.appendChild(node) } } return Promise.allSettled(promises) @@ -168,7 +176,12 @@ export const [provideVisualizationStore, useVisualizationStore] = createContextS await projectRoot, await proj.dataConnection, ).then(async (viz) => { - await loadScripts(viz) + const styles = viz.styles ?? [] + const scripts = viz.scripts ?? [] + await Promise.allSettled([ + loadAsync(styles, 'styles', document.head, loadedStyles), + loadAsync(scripts, 'scripts', scriptsNode, loadedScripts), + ]) return viz }) if (key) cache.set(key, vizPromise) @@ -279,7 +292,11 @@ export const [provideVisualizationStore, useVisualizationStore] = createContextS async function resolveBuiltinVisualization(type: string) { const module = builtinVisualizationsByName[type] if (!module) throw new Error(`Unknown visualization type: ${type}`) - await loadScripts(module) + const { scripts, styles } = module + await Promise.allSettled([ + loadAsync(styles ?? [], 'styles', document.head, loadedStyles), + loadAsync(scripts ?? [], 'scripts', scriptsNode, loadedScripts), + ]) return module } diff --git a/app/ide-desktop/client/src/globals.d.ts b/app/ide-desktop/client/src/globals.d.ts index 0b30a96221ff..85645d320f4f 100644 --- a/app/ide-desktop/client/src/globals.d.ts +++ b/app/ide-desktop/client/src/globals.d.ts @@ -158,6 +158,7 @@ declare global { readonly fileBrowserApi?: FileBrowserApi readonly projectManagementApi?: ProjectManagementApi readonly versionInfo?: VersionInfo + readonly mapBoxApiToken: () => string toggleDevtools: () => void } diff --git a/app/ide-desktop/client/src/preload.ts b/app/ide-desktop/client/src/preload.ts index 855e4fd241a9..c49383057caa 100644 --- a/app/ide-desktop/client/src/preload.ts +++ b/app/ide-desktop/client/src/preload.ts @@ -31,6 +31,7 @@ const NAVIGATION_API_KEY = 'navigationApi' const MENU_API_KEY = 'menuApi' const SYSTEM_API_KEY = 'systemApi' const VERSION_INFO_KEY = 'versionInfo' +const MAPBOX_API_TOKEN_KEY = 'mapBoxApiToken' // ========================= // === exposeInMainWorld === @@ -205,3 +206,9 @@ exposeInMainWorld(SYSTEM_API_KEY, { // ==================== exposeInMainWorld(VERSION_INFO_KEY, debug.VERSION_INFO) + +// ================== +// === MapBox API === +// ================== + +exposeInMainWorld(MAPBOX_API_TOKEN_KEY, () => process.env.ENSO_IDE_MAPBOX_API_TOKEN || '') diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cd8885031bbd..2d006e6f2eff 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -456,8 +456,8 @@ importers: specifier: ^21.1.1 version: 21.1.7 '@types/mapbox-gl': - specifier: ^2.7.13 - version: 2.7.21 + specifier: ^3.4.1 + version: 3.4.1 '@types/node': specifier: ^22.9.0 version: 22.9.0 @@ -3240,8 +3240,8 @@ packages: '@types/linkify-it@5.0.0': resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} - '@types/mapbox-gl@2.7.21': - resolution: {integrity: sha512-Dx9MuF2kKgT/N22LsMUB4b3acFZh9clVqz9zv1fomoiPoBrJolwYxpWA/9LPO/2N0xWbKi4V+pkjTaFkkx/4wA==} + '@types/mapbox-gl@3.4.1': + resolution: {integrity: sha512-NsGKKtgW93B+UaLPti6B7NwlxYlES5DpV5Gzj9F75rK5ALKsqSk15CiEHbOnTr09RGbr6ZYiCdI+59NNNcAImg==} '@types/markdown-it@12.2.3': resolution: {integrity: sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==} @@ -11433,7 +11433,7 @@ snapshots: '@types/linkify-it@5.0.0': {} - '@types/mapbox-gl@2.7.21': + '@types/mapbox-gl@3.4.1': dependencies: '@types/geojson': 7946.0.14