diff --git a/common/constants/shared.ts b/common/constants/shared.ts index 7d1ed801..1fe05b96 100644 --- a/common/constants/shared.ts +++ b/common/constants/shared.ts @@ -12,5 +12,5 @@ export const MAX_FILE_PAYLOAD_SIZE_IN_MB = 25; export const MAX_FILE_PAYLOAD_SIZE = fromMBtoBytes(MAX_FILE_PAYLOAD_SIZE_IN_MB); export const PLUGIN_ID = 'customImportMap'; export const PLUGIN_NAME = 'customImportMap'; -export const PLUGIN_NAVIGATION_BAR_TILE = 'Maps'; -export const PLUGIN_NAVIGATION_BAR_ID = 'maps-dashboards'; +export const MAPS_APP_DISPLAY_NAME = 'Maps'; +export const MAPS_APP_ID = 'maps-dashboards'; diff --git a/common/index.ts b/common/index.ts index 80f243ba..7a1dff36 100644 --- a/common/index.ts +++ b/common/index.ts @@ -9,7 +9,7 @@ import { MAX_FILE_PAYLOAD_SIZE, MAX_FILE_PAYLOAD_SIZE_IN_MB, PLUGIN_ID, - PLUGIN_NAVIGATION_BAR_ID, + MAPS_APP_ID, PLUGIN_NAME, } from './constants/shared'; @@ -19,7 +19,7 @@ export { MAX_FILE_PAYLOAD_SIZE, MAX_FILE_PAYLOAD_SIZE_IN_MB, PLUGIN_ID, - PLUGIN_NAVIGATION_BAR_ID, + MAPS_APP_ID, PLUGIN_NAME, }; @@ -44,6 +44,10 @@ export const MAX_LAYER_NAME_LIMIT = 35; export const MAX_LONGITUDE = 180; export const MIN_LONGITUDE = -180; export const NEW_MAP_LAYER_DEFAULT_PREFIX = 'New layer'; +export const MAP_SAVED_OBJECT_TYPE = 'map'; +// TODO: Replace with actual app icon +export const MAPS_APP_ICON = 'gisApp'; +export const MAPS_VISUALIZATION_DESCRIPTION = 'Create map visualization with multiple layers'; // Starting position [lng, lat] and zoom export const MAP_INITIAL_STATE = { diff --git a/cypress/integration/add_map_to_dashboard.spec.js b/cypress/integration/add_map_to_dashboard.spec.js new file mode 100644 index 00000000..fbb3da8f --- /dev/null +++ b/cypress/integration/add_map_to_dashboard.spec.js @@ -0,0 +1,36 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { BASE_PATH } from '../utils/constants'; + +describe('Add map to dashboard', () => { + before(() => { + cy.visit(`${BASE_PATH}/app/home#/tutorial_directory/sampleData`, { + retryOnStatusCodeFailure: true, + timeout: 60000, + }); + cy.get('div[data-test-subj="sampleDataSetCardflights"]', { timeout: 60000 }) + .contains(/(Add|View) data/) + .click(); + cy.wait(60000); + }); + + it('Add new map to dashboard', () => { + const testMapName = 'saved-map-' + Date.now().toString(); + cy.visit(`${BASE_PATH}/app/dashboards`); + cy.get('button[data-test-subj="newItemButton"]').click(); + cy.get('button[data-test-subj="dashboardAddNewPanelButton"]').click(); + cy.get('button[data-test-subj="visType-customImportMap"]').click(); + cy.wait(5000).get('button[data-test-subj="mapSaveButton"]').click(); + cy.wait(5000).get('[data-test-subj="savedObjectTitle"]').type(testMapName); + cy.wait(5000).get('[data-test-subj="confirmSaveSavedObjectButton"]').click(); + cy.get('.embPanel__titleText').should('contain', testMapName); + }); + + after(() => { + cy.visit(`${BASE_PATH}/app/home#/tutorial_directory`); + cy.get('button[data-test-subj="removeSampleDataSetflights"]').should('be.visible').click(); + }); +}); diff --git a/opensearch_dashboards.json b/opensearch_dashboards.json index 306f3d37..e8abd481 100644 --- a/opensearch_dashboards.json +++ b/opensearch_dashboards.json @@ -4,6 +4,6 @@ "opensearchDashboardsVersion": "3.0.0", "server": true, "ui": true, - "requiredPlugins": ["regionMap", "opensearchDashboardsReact", "navigation", "savedObjects", "data"], + "requiredPlugins": ["regionMap", "opensearchDashboardsReact", "opensearchDashboardsUtils", "navigation", "savedObjects", "data", "embeddable", "visualizations"], "optionalPlugins": ["home"] } diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..b786a608 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,249 @@ +{ + "name": "mapsDashboards", + "version": "3.0.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@mapbox/geojson-rewind": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@mapbox/geojson-rewind/-/geojson-rewind-0.5.2.tgz", + "integrity": "sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA==", + "requires": { + "get-stream": "^6.0.1", + "minimist": "^1.2.6" + } + }, + "@mapbox/jsonlint-lines-primitives": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz", + "integrity": "sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==" + }, + "@mapbox/mapbox-gl-supported": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@mapbox/mapbox-gl-supported/-/mapbox-gl-supported-2.0.1.tgz", + "integrity": "sha512-HP6XvfNIzfoMVfyGjBckjiAOQK9WfX0ywdLubuPMPv+Vqf5fj0uCbgBQYpiqcWZT6cbyyRnTSXDheT1ugvF6UQ==" + }, + "@mapbox/point-geometry": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz", + "integrity": "sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ==" + }, + "@mapbox/tiny-sdf": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.0.5.tgz", + "integrity": "sha512-OhXt2lS//WpLdkqrzo/KwB7SRD8AiNTFFzuo9n14IBupzIMa67yGItcK7I2W9D8Ghpa4T04Sw9FWsKCJG50Bxw==" + }, + "@mapbox/unitbezier": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz", + "integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==" + }, + "@mapbox/vector-tile": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-1.3.1.tgz", + "integrity": "sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw==", + "requires": { + "@mapbox/point-geometry": "~0.1.0" + } + }, + "@mapbox/whoots-js": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz", + "integrity": "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==" + }, + "@types/geojson": { + "version": "7946.0.10", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.10.tgz", + "integrity": "sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA==" + }, + "@types/mapbox__point-geometry": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@types/mapbox__point-geometry/-/mapbox__point-geometry-0.1.2.tgz", + "integrity": "sha512-D0lgCq+3VWV85ey1MZVkE8ZveyuvW5VAfuahVTQRpXFQTxw03SuIf1/K4UQ87MMIXVKzpFjXFiFMZzLj2kU+iA==" + }, + "@types/mapbox__vector-tile": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@types/mapbox__vector-tile/-/mapbox__vector-tile-1.3.0.tgz", + "integrity": "sha512-kDwVreQO5V4c8yAxzZVQLE5tyWF+IPToAanloQaSnwfXmIcJ7cyOrv8z4Ft4y7PsLYmhWXmON8MBV8RX0Rgr8g==", + "requires": { + "@types/geojson": "*", + "@types/mapbox__point-geometry": "*", + "@types/pbf": "*" + } + }, + "@types/pbf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/pbf/-/pbf-3.0.2.tgz", + "integrity": "sha512-EDrLIPaPXOZqDjrkzxxbX7UlJSeQVgah3i0aA4pOSzmK9zq3BIh7/MZIQxED7slJByvKM4Gc6Hypyu2lJzh3SQ==" + }, + "csscolorparser": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/csscolorparser/-/csscolorparser-1.0.3.tgz", + "integrity": "sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w==" + }, + "earcut": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz", + "integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==" + }, + "geojson-vt": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-3.2.1.tgz", + "integrity": "sha512-EvGQQi/zPrDA6zr6BnJD/YhwAkBP8nnJ9emh3EnHQKVMfg/MRVtPbMYdgVy/IaEmn4UfagD2a6fafPDL5hbtwg==" + }, + "get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==" + }, + "gl-matrix": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.3.tgz", + "integrity": "sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA==" + }, + "global-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", + "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", + "requires": { + "ini": "^1.3.5", + "kind-of": "^6.0.2", + "which": "^1.3.1" + } + }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + }, + "ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "kdbush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-3.0.0.tgz", + "integrity": "sha512-hRkd6/XW4HTsA9vjVpY9tuXJYLSlelnkTmVFu4M9/7MIYQtFcHpbugAU7UbOfjOiVSVYl2fqgBuJ32JUmRo5Ew==" + }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==" + }, + "maplibre-gl": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-2.4.0.tgz", + "integrity": "sha512-csNFylzntPmHWidczfgCZpvbTSmhaWvLRj9e1ezUDBEPizGgshgm3ea1T5TCNEEBq0roauu7BPuRZjA3wO4KqA==", + "requires": { + "@mapbox/geojson-rewind": "^0.5.2", + "@mapbox/jsonlint-lines-primitives": "^2.0.2", + "@mapbox/mapbox-gl-supported": "^2.0.1", + "@mapbox/point-geometry": "^0.1.0", + "@mapbox/tiny-sdf": "^2.0.5", + "@mapbox/unitbezier": "^0.0.1", + "@mapbox/vector-tile": "^1.3.1", + "@mapbox/whoots-js": "^3.1.0", + "@types/geojson": "^7946.0.10", + "@types/mapbox__point-geometry": "^0.1.2", + "@types/mapbox__vector-tile": "^1.3.0", + "@types/pbf": "^3.0.2", + "csscolorparser": "~1.0.3", + "earcut": "^2.2.4", + "geojson-vt": "^3.2.1", + "gl-matrix": "^3.4.3", + "global-prefix": "^3.0.0", + "murmurhash-js": "^1.0.0", + "pbf": "^3.2.1", + "potpack": "^1.0.2", + "quickselect": "^2.0.0", + "supercluster": "^7.1.5", + "tinyqueue": "^2.0.3", + "vt-pbf": "^3.1.3" + } + }, + "minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" + }, + "murmurhash-js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz", + "integrity": "sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==" + }, + "pbf": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/pbf/-/pbf-3.2.1.tgz", + "integrity": "sha512-ClrV7pNOn7rtmoQVF4TS1vyU0WhYRnP92fzbfF75jAIwpnzdJXf8iTd4CMEqO4yUenH6NDqLiwjqlh6QgZzgLQ==", + "requires": { + "ieee754": "^1.1.12", + "resolve-protobuf-schema": "^2.1.0" + } + }, + "potpack": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz", + "integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==" + }, + "protocol-buffers-schema": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz", + "integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==" + }, + "quickselect": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz", + "integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==" + }, + "resolve-protobuf-schema": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz", + "integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==", + "requires": { + "protocol-buffers-schema": "^3.3.1" + } + }, + "supercluster": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-7.1.5.tgz", + "integrity": "sha512-EulshI3pGUM66o6ZdH3ReiFcvHpM3vAigyK+vcxdjpJyEbIIrtbmBdY23mGgnI24uXiGFvrGq9Gkum/8U7vJWg==", + "requires": { + "kdbush": "^3.0.0" + } + }, + "tinyqueue": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-2.0.3.tgz", + "integrity": "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==" + }, + "uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==" + }, + "vt-pbf": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/vt-pbf/-/vt-pbf-3.1.3.tgz", + "integrity": "sha512-2LzDFzt0mZKZ9IpVF2r69G9bXaP2Q2sArJCmcCgvfTdCCZzSyz4aCLoQyUilu37Ll56tCblIZrXFIjNUpGIlmA==", + "requires": { + "@mapbox/point-geometry": "0.1.0", + "@mapbox/vector-tile": "^1.3.1", + "pbf": "^3.2.1" + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "requires": { + "isexe": "^2.0.0" + } + } + } +} diff --git a/public/components/app.tsx b/public/components/app.tsx index 79e07c1c..a0122f46 100644 --- a/public/components/app.tsx +++ b/public/components/app.tsx @@ -24,15 +24,13 @@ export const MapsDashboardsApp = ({ mapConfig }: Props) => { return ( -
- - } - /> - } /> - -
+ + } + /> + } /> +
); diff --git a/public/components/layer_control_panel/layer_control_panel.tsx b/public/components/layer_control_panel/layer_control_panel.tsx index 802379c5..65b5761c 100644 --- a/public/components/layer_control_panel/layer_control_panel.tsx +++ b/public/components/layer_control_panel/layer_control_panel.tsx @@ -8,7 +8,6 @@ import { DropResult, EuiButtonEmpty, EuiButtonIcon, - EuiConfirmModal, EuiDragDropContext, EuiDraggable, EuiDroppable, @@ -60,6 +59,7 @@ interface Props { mapState: MapState; zoom: number; mapConfig: ConfigSchema; + inDashboardMode: boolean; } export const LayerControlPanel = memo( @@ -72,6 +72,7 @@ export const LayerControlPanel = memo( mapState, zoom, mapConfig, + inDashboardMode, }: Props) => { const { services } = useOpenSearchDashboards(); const { @@ -321,8 +322,13 @@ export const LayerControlPanel = memo( return visibleLayers.includes(layer); }; + if (inDashboardMode) { + return null; + } + + let content; if (isLayerControlVisible) { - return ( + content = ( ); + } else { + content = ( + + setIsLayerControlVisible((visible) => !visible)} + aria-label="Show layer control" + title="Expand layers panel" + /> + + ); } - return ( - - setIsLayerControlVisible((visible) => !visible)} - aria-label="Show layer control" - title="Expand layers panel" - /> - - ); + return
{content}
; } ); diff --git a/public/components/map_container/map_container.scss b/public/components/map_container/map_container.scss index 7453ca94..a4ee3065 100644 --- a/public/components/map_container/map_container.scss +++ b/public/components/map_container/map_container.scss @@ -6,10 +6,10 @@ @import "maplibre-gl/dist/maplibre-gl.css"; @import "../../variables"; -/* stylelint-disable no-empty-source */ -.map-container { - width: 100%; - min-height: calc(100vh - #{$mapHeaderOffset}); +.mapAppContainer, .map-page, .map-container, .map-main{ + display: flex; + flex-direction: column; + flex: 1; } .maplibregl-ctrl-top-left { diff --git a/public/components/map_container/map_container.tsx b/public/components/map_container/map_container.tsx index a17708b7..8cf58546 100644 --- a/public/components/map_container/map_container.tsx +++ b/public/components/map_container/map_container.tsx @@ -6,18 +6,26 @@ import React, { useEffect, useRef, useState } from 'react'; import { EuiPanel } from '@elastic/eui'; import { LngLat, Map as Maplibre, NavigationControl, Popup, MapEventType } from 'maplibre-gl'; -import { debounce } from 'lodash'; +import { debounce, throttle } from 'lodash'; import { LayerControlPanel } from '../layer_control_panel'; import './map_container.scss'; import { MAP_INITIAL_STATE, DASHBOARDS_MAPS_LAYER_TYPE } from '../../../common'; import { MapLayerSpecification } from '../../model/mapLayerType'; -import { IndexPattern } from '../../../../../src/plugins/data/public'; +import { + IndexPattern, + RefreshInterval, + TimeRange, + Filter, + Query, +} from '../../../../../src/plugins/data/public'; import { MapState } from '../../model/mapState'; import { createPopup, getPopupLocation, isTooltipEnabledLayer } from '../tooltip/create_tooltip'; import { handleDataLayerRender } from '../../model/layerRenderController'; import { useOpenSearchDashboards } from '../../../../../src/plugins/opensearch_dashboards_react/public'; +import { ResizeChecker } from '../../../../../src/plugins/opensearch_dashboards_utils/public'; import { MapServices } from '../../types'; import { ConfigSchema } from '../../../common/config'; +import { referenceLayerTypeLookup } from '../../model/layersFunctions'; interface MapContainerProps { setLayers: (layers: MapLayerSpecification[]) => void; @@ -27,6 +35,11 @@ interface MapContainerProps { maplibreRef: React.MutableRefObject; mapState: MapState; mapConfig: ConfigSchema; + inDashboardMode: boolean; + timeRange?: TimeRange; + refreshConfig?: RefreshInterval; + filters?: Filter[]; + query?: Query; } export const MapContainer = ({ @@ -37,6 +50,11 @@ export const MapContainer = ({ maplibreRef, mapState, mapConfig, + inDashboardMode, + timeRange, + refreshConfig, + filters, + query, }: MapContainerProps) => { const { services } = useOpenSearchDashboards(); const mapContainer = useRef(null); @@ -68,6 +86,28 @@ export const MapContainer = ({ maplibreInstance.on('move', () => { return setZoom(Number(maplibreInstance.getZoom().toFixed(2))); }); + + // By default, Maplibre only auto resize map window when browser size changes, but in dashboard mode, we need + // manually resize map window size when map panel size changes + const mapContainerElement: HTMLElement | null = document.querySelector('.map-page'); + let resizeChecker: ResizeChecker; + if (mapContainerElement) { + resizeChecker = new ResizeChecker(mapContainerElement); + if (inDashboardMode) { + resizeChecker.on( + 'resize', + throttle(() => { + maplibreInstance?.resize(); + }, 300) + ); + } + } + return () => { + maplibreInstance.remove(); + if (resizeChecker) { + resizeChecker.destroy(); + } + }; }, []); // Create onClick tooltip for each layer features that has tooltip enabled @@ -166,8 +206,43 @@ export const MapContainer = ({ }; }, [layers, mapState, services]); + // Update data layers when state bar time range, filters and query changes + useEffect(() => { + layers.forEach((layer: MapLayerSpecification) => { + if (referenceLayerTypeLookup[layer.type]) { + return; + } + handleDataLayerRender( + layer, + mapState, + services, + maplibreRef, + undefined, + timeRange, + filters, + query + ); + }); + }, [timeRange, mapState, filters]); + + // Update data layers when state bar enable auto refresh + useEffect(() => { + let intervalId: NodeJS.Timeout | undefined; + if (refreshConfig && !refreshConfig.pause) { + intervalId = setInterval(() => { + layers.forEach((layer: MapLayerSpecification) => { + if (referenceLayerTypeLookup[layer.type]) { + return; + } + handleDataLayerRender(layer, mapState, services, maplibreRef, undefined, timeRange); + }); + }, refreshConfig.value); + } + return () => clearInterval(intervalId); + }, [refreshConfig]); + return ( -
+
-
- {mounted && ( - - )} -
+ {mounted && ( + + )}
); diff --git a/public/components/map_page/index.ts b/public/components/map_page/index.ts index a79e0689..a43e82ae 100644 --- a/public/components/map_page/index.ts +++ b/public/components/map_page/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export { MapPage } from './map_page'; +export { MapPage, MapComponent } from './map_page'; diff --git a/public/components/map_page/map_page.tsx b/public/components/map_page/map_page.tsx index 26beef27..0151d9a0 100644 --- a/public/components/map_page/map_page.tsx +++ b/public/components/map_page/map_page.tsx @@ -5,8 +5,8 @@ import React, { useEffect, useRef, useState } from 'react'; import { useParams } from 'react-router-dom'; -import { SimpleSavedObject } from 'opensearch-dashboards/public'; import { Map as Maplibre } from 'maplibre-gl'; +import { SimpleSavedObject } from '../../../../../src/core/public'; import { MapContainer } from '../map_container'; import { MapTopNavMenu } from '../map_top_nav'; import { MapServices } from '../../types'; @@ -17,22 +17,45 @@ import { MAP_LAYER_DEFAULT_NAME, OPENSEARCH_MAP_LAYER, } from '../../../common'; +import { MapLayerSpecification } from '../../model/mapLayerType'; import { getLayerConfigMap, getInitialMapState } from '../../utils/getIntialConfig'; -import { IndexPattern } from '../../../../../src/plugins/data/public'; +import { + Filter, + IndexPattern, + RefreshInterval, + TimeRange, + Query, +} from '../../../../../src/plugins/data/public'; import { MapState } from '../../model/mapState'; import { ConfigSchema } from '../../../common/config'; -interface Props { +interface MapPageProps { mapConfig: ConfigSchema; } -export const MapPage = ({ mapConfig }: Props) => { +interface MapComponentProps { + mapConfig: ConfigSchema; + mapIdFromSavedObject: string; + timeRange?: TimeRange; + inDashboardMode: boolean; + refreshConfig?: RefreshInterval; + filters?: Filter[]; + query?: Query; +} +export const MapComponent = ({ + mapIdFromSavedObject, + mapConfig, + timeRange, + inDashboardMode, + refreshConfig, + filters, + query, +}: MapComponentProps) => { const { services } = useOpenSearchDashboards(); const { savedObjects: { client: savedObjectsClient }, } = services; const [layers, setLayers] = useState([]); - const { id: mapIdFromUrl } = useParams<{ id: string }>(); const [savedMapObject, setSavedMapObject] = useState | null>(); const [layersIndexPatterns, setLayersIndexPatterns] = useState([]); @@ -40,8 +63,8 @@ export const MapPage = ({ mapConfig }: Props) => { const [mapState, setMapState] = useState(getInitialMapState()); useEffect(() => { - if (mapIdFromUrl) { - savedObjectsClient.get('map', mapIdFromUrl).then((res) => { + if (mapIdFromSavedObject) { + savedObjectsClient.get('map', mapIdFromSavedObject).then((res) => { setSavedMapObject(res); const layerList: MapLayerSpecification[] = JSON.parse(res.attributes.layerList as string); const savedMapState: MapState = JSON.parse(res.attributes.mapState as string); @@ -58,25 +81,30 @@ export const MapPage = ({ mapConfig }: Props) => { setLayersIndexPatterns(savedIndexPatterns); }); } else { - const initialDefaultLayer: MapLayerSpecification = getLayerConfigMap(mapConfig)[ - OPENSEARCH_MAP_LAYER.type - ]; + const initialDefaultLayer: MapLayerSpecification = + getLayerConfigMap(mapConfig)[OPENSEARCH_MAP_LAYER.type]; initialDefaultLayer.name = MAP_LAYER_DEFAULT_NAME; setLayers([initialDefaultLayer]); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return ( -
- +
+ {inDashboardMode ? null : ( + + )} + { maplibreRef={maplibreRef} mapState={mapState} mapConfig={mapConfig} + inDashboardMode={inDashboardMode} + timeRange={timeRange} + refreshConfig={refreshConfig} + filters={filters} + query={query} />
); }; + +export const MapPage = ({ mapConfig }: MapPageProps) => { + const { id: mapId } = useParams<{ id: string }>(); + return ( + + ); +}; diff --git a/public/components/map_top_nav/get_top_nav_config.tsx b/public/components/map_top_nav/get_top_nav_config.tsx index 4269c356..d69f8fc6 100644 --- a/public/components/map_top_nav/get_top_nav_config.tsx +++ b/public/components/map_top_nav/get_top_nav_config.tsx @@ -7,15 +7,14 @@ import React from 'react'; import { i18n } from '@osd/i18n'; import { TopNavMenuData } from '../../../../../src/plugins/navigation/public'; import { - OnSaveProps, SavedObjectSaveModalOrigin, showSaveModal, checkForDuplicateTitle, + SavedObjectSaveOpts, } from '../../../../../src/plugins/saved_objects/public'; import { MapServices } from '../../types'; import { MapState } from '../../model/mapState'; - -const SAVED_OBJECT_TYPE = 'map'; +import { MAP_SAVED_OBJECT_TYPE } from '../../../common'; interface GetTopNavConfigParams { mapIdFromUrl: string; @@ -25,16 +24,11 @@ interface GetTopNavConfigParams { setTitle: (title: string) => void; setDescription: (description: string) => void; mapState: MapState; + originatingApp?: string; } export const getTopNavConfig = ( - { - notifications: { toasts }, - i18n: { Context: I18nContext }, - savedObjects: { client: savedObjectsClient }, - history, - overlays, - }: MapServices, + services: MapServices, { mapIdFromUrl, layers, @@ -43,8 +37,15 @@ export const getTopNavConfig = ( setTitle, setDescription, mapState, + originatingApp, }: GetTopNavConfigParams ) => { + const { + embeddable, + i18n: { Context: I18nContext }, + scopedHistory, + } = services; + const stateTransfer = embeddable.getStateTransfer(scopedHistory); const topNavConfig: TopNavMenuData[] = [ { iconType: 'save', @@ -53,65 +54,8 @@ export const getTopNavConfig = ( label: i18n.translate('maps.topNav.saveMapButtonLabel', { defaultMessage: `Save`, }), - run: (_anchorElement) => { - const onModalSave = async ({ newTitle, newDescription, onTitleDuplicate }: OnSaveProps) => { - let newlySavedMap; - const saveAttributes = { - title: newTitle, - description: newDescription, - layerList: JSON.stringify(layers), - mapState: JSON.stringify(mapState), - }; - try { - await checkForDuplicateTitle( - { - title: newTitle, - lastSavedTitle: title, - copyOnSave: false, - getDisplayName: () => SAVED_OBJECT_TYPE, - getOpenSearchType: () => SAVED_OBJECT_TYPE, - }, - false, - onTitleDuplicate, - { - savedObjectsClient, - overlays, - } - ); - } catch (_error) { - return {}; - } - if (mapIdFromUrl) { - // edit existing map - newlySavedMap = await savedObjectsClient.update( - SAVED_OBJECT_TYPE, - mapIdFromUrl, - saveAttributes - ); - } else { - // save new map - newlySavedMap = await savedObjectsClient.create(SAVED_OBJECT_TYPE, saveAttributes); - } - const id = newlySavedMap.id; - if (id) { - history.push({ - ...history.location, - pathname: `${id}`, - }); - setTitle(newTitle); - setDescription(newDescription); - toasts.addSuccess({ - title: i18n.translate('map.topNavMenu.saveMap.successNotificationText', { - defaultMessage: `Saved ${newTitle}`, - values: { - visTitle: newTitle, - }, - }), - }); - } - return { id }; - }; - + testId: 'mapSaveButton', + run: (_anchorElement: any) => { const documentInfo = { title, description, @@ -120,9 +64,20 @@ export const getTopNavConfig = ( const saveModal = ( {}} + originatingApp={originatingApp} + getAppNameFromId={stateTransfer.getAppNameFromId} /> ); showSaveModal(saveModal, I18nContext); @@ -131,3 +86,121 @@ export const getTopNavConfig = ( ]; return topNavConfig; }; + +export const onGetSave = ( + title: string, + originatingApp: string | undefined, + mapIdFromUrl: string, + services: MapServices, + layers: any, + mapState: MapState, + setTitle: (title: string) => void, + setDescription: (description: string) => void +) => { + const onSave = async ({ + newTitle, + newDescription, + onTitleDuplicate, + returnToOrigin, + }: SavedObjectSaveOpts & { + newTitle: string; + newCopyOnSave: boolean; + returnToOrigin: boolean; + newDescription?: string; + }) => { + const { + savedObjects: { client: savedObjectsClient }, + history, + toastNotifications, + overlays, + embeddable, + application, + } = services; + const stateTransfer = embeddable.getStateTransfer(); + let newlySavedMap; + const saveAttributes = { + title: newTitle, + description: newDescription, + layerList: JSON.stringify(layers), + mapState: JSON.stringify(mapState), + }; + try { + await checkForDuplicateTitle( + { + title: newTitle, + lastSavedTitle: title, + copyOnSave: false, + getDisplayName: () => MAP_SAVED_OBJECT_TYPE, + getOpenSearchType: () => MAP_SAVED_OBJECT_TYPE, + }, + false, + onTitleDuplicate, + { + savedObjectsClient, + overlays, + } + ); + } catch (_error) { + return {}; + } + try { + if (mapIdFromUrl) { + // edit existing map + newlySavedMap = await savedObjectsClient.update( + MAP_SAVED_OBJECT_TYPE, + mapIdFromUrl, + saveAttributes + ); + } else { + // save new map + newlySavedMap = await savedObjectsClient.create(MAP_SAVED_OBJECT_TYPE, saveAttributes); + } + const id = newlySavedMap.id; + if (id) { + history.push({ + ...history.location, + pathname: `${id}`, + }); + setTitle(newTitle); + if (newDescription) { + setDescription(newDescription); + } + toastNotifications.addSuccess({ + title: i18n.translate('map.topNavMenu.saveMap.successNotificationText', { + defaultMessage: `Saved ${newTitle}`, + values: { + visTitle: newTitle, + }, + }), + }); + if (originatingApp && returnToOrigin) { + // create or edit map directly from another app, such as `dashboard` + if (!mapIdFromUrl && stateTransfer) { + // create new embeddable to transfer to originatingApp + await stateTransfer.navigateToWithEmbeddablePackage(originatingApp, { + state: { type: MAP_SAVED_OBJECT_TYPE, input: { savedObjectId: id } }, + }); + return { id }; + } else { + // update an existing visBuilder from another app + application.navigateToApp(originatingApp); + } + } + } + return { id }; + } catch (error: any) { + toastNotifications.addDanger({ + title: i18n.translate('maps.topNavMenu.saveVisualization.failureNotificationText', { + defaultMessage: `Error on saving ${newTitle}`, + values: { + visTitle: newTitle, + }, + }), + text: error.message, + 'data-test-subj': 'saveMapError', + }); + return { error }; + } + }; + return onSave; +}; diff --git a/public/components/map_top_nav/top_nav_menu.tsx b/public/components/map_top_nav/top_nav_menu.tsx index 15b2fbe3..b5b9a184 100644 --- a/public/components/map_top_nav/top_nav_menu.tsx +++ b/public/components/map_top_nav/top_nav_menu.tsx @@ -3,10 +3,10 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useCallback, useEffect, useState } from 'react'; -import { SimpleSavedObject } from 'opensearch-dashboards/public'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { SimpleSavedObject } from '../../../../../src/core/public'; import { IndexPattern, Query, TimeRange } from '../../../../../src/plugins/data/public'; -import { DASHBOARDS_MAPS_LAYER_TYPE, PLUGIN_NAVIGATION_BAR_ID } from '../../../common'; +import { DASHBOARDS_MAPS_LAYER_TYPE, MAPS_APP_ID } from '../../../common'; import { getTopNavConfig } from './get_top_nav_config'; import { useOpenSearchDashboards } from '../../../../../src/plugins/opensearch_dashboards_react/public'; import { MapServices } from '../../types'; @@ -24,11 +24,16 @@ interface MapTopNavMenuProps { maplibreRef: any; mapState: MapState; setMapState: (mapState: MapState) => void; + inDashboardMode: boolean; + timeRange?: TimeRange; + originatingApp?: string; } export const MapTopNavMenu = ({ mapIdFromUrl, savedMapObject, + inDashboardMode, + timeRange, layers, layersIndexPatterns, maplibreRef, @@ -43,6 +48,8 @@ export const MapTopNavMenu = ({ }, chrome, application: { navigateToApp }, + embeddable, + scopedHistory, } = services; const [title, setTitle] = useState(''); @@ -52,6 +59,7 @@ export const MapTopNavMenu = ({ const [queryConfig, setQueryConfig] = useState({ query: '', language: 'kuery' }); const [refreshIntervalValue, setRefreshIntervalValue] = useState(60000); const [isRefreshPaused, setIsRefreshPaused] = useState(false); + const [originatingApp, setOriginatingApp] = useState(); const changeTitle = useCallback( (newTitle: string) => { chrome.setBreadcrumbs(getSavedMapBreadcrumbs(newTitle, navigateToApp)); @@ -60,6 +68,14 @@ export const MapTopNavMenu = ({ [chrome, navigateToApp] ); + useEffect(() => { + const { originatingApp: value } = + embeddable + .getStateTransfer(scopedHistory) + .getIncomingEditorState({ keysToRemoveAfterFetch: ['id', 'input'] }) || {}; + setOriginatingApp(value); + }, [embeddable, scopedHistory]); + useEffect(() => { if (savedMapObject) { setTitle(savedMapObject.attributes.title); @@ -73,10 +89,9 @@ export const MapTopNavMenu = ({ const refreshDataLayerRender = () => { layers.forEach((layer: MapLayerSpecification) => { - if (layer.type === DASHBOARDS_MAPS_LAYER_TYPE.OPENSEARCH_MAP) { - return; + if (layer.type === DASHBOARDS_MAPS_LAYER_TYPE.DOCUMENTS) { + handleDataLayerRender(layer, mapState, services, maplibreRef, undefined); } - handleDataLayerRender(layer, mapState, services, maplibreRef, undefined); }); }; @@ -90,13 +105,17 @@ export const MapTopNavMenu = ({ }; useEffect(() => { - setDateFrom(mapState.timeRange.from); - setDateTo(mapState.timeRange.to); + if (!inDashboardMode) { + setDateFrom(mapState.timeRange.from); + setDateTo(mapState.timeRange.to); + } else { + setDateFrom(timeRange!.from); + setDateTo(timeRange!.to); + } setQueryConfig(mapState.query); setIsRefreshPaused(mapState.refreshInterval.pause); setRefreshIntervalValue(mapState.refreshInterval.value); - refreshDataLayerRender(); - }, [mapState]); + }, [mapState, timeRange]); const onRefreshChange = useCallback( ({ isPaused, refreshInterval }: { isPaused: boolean; refreshInterval: number }) => { @@ -106,23 +125,28 @@ export const MapTopNavMenu = ({ [] ); + const config = useMemo(() => { + return getTopNavConfig(services, { + mapIdFromUrl, + layers, + title, + description, + setTitle, + setDescription, + mapState, + originatingApp, + }); + }, [services, mapIdFromUrl, layers, title, description, mapState, originatingApp]); + return ( { const { @@ -39,7 +39,7 @@ export const MapsList = () => { }, [docTitle, navigateToApp, setBreadcrumbs]); const navigateToSavedMapPage = (id: string) => { - navigateToApp(PLUGIN_NAVIGATION_BAR_ID, { path: `/${id}` }); + navigateToApp(MAPS_APP_ID, { path: `/${id}` }); }; const tableColumns = [ @@ -70,7 +70,7 @@ export const MapsList = () => { ]; const navigateToCreateMapPage = () => { - navigateToApp(PLUGIN_NAVIGATION_BAR_ID, { path: APP_PATH.CREATE_MAP }); + navigateToApp(MAPS_APP_ID, { path: APP_PATH.CREATE_MAP }); }; const fetchMaps = useCallback(async (): Promise<{ diff --git a/public/embeddable/index.ts b/public/embeddable/index.ts new file mode 100644 index 00000000..9687b6ce --- /dev/null +++ b/public/embeddable/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './map_embeddable'; +export * from './map_embeddable_factory'; diff --git a/public/embeddable/map_component.tsx b/public/embeddable/map_component.tsx new file mode 100644 index 00000000..a2fb58f4 --- /dev/null +++ b/public/embeddable/map_component.tsx @@ -0,0 +1,57 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useEffect, useState } from 'react'; +import { + withEmbeddableSubscription, + EmbeddableOutput, +} from '../../../../src/plugins/embeddable/public'; +import { MapEmbeddable, MapInput } from './map_embeddable'; +import { MapComponent } from '../components/map_page/'; +import { OpenSearchDashboardsContextProvider } from '../../../../src/plugins/opensearch_dashboards_react/public'; +import { MapServices } from '../types'; +import { TimeRange } from '../../../../src/plugins/data/common'; + +interface Props { + embeddable: MapEmbeddable; + input: MapInput; + output: EmbeddableOutput; +} +export function MapEmbeddableComponentInner({ embeddable, input }: Props) { + const [timeRange, setTimeRange] = useState(input.timeRange); + const [refreshConfig, setRefreshConfig] = useState(input.refreshConfig); + const [filters, setFilters] = useState(input.filters); + const [query, setQuery] = useState(input.query); + const services: MapServices = { + ...embeddable.getServiceSettings(), + }; + + useEffect(() => { + setTimeRange(input.timeRange); + setRefreshConfig(input.refreshConfig); + setFilters(input.filters); + setQuery(input.query); + }, [input.refreshConfig, input.timeRange, input.filters, input.query]); + + return ( + + + + ); +} + +export const MapEmbeddableComponent = withEmbeddableSubscription< + MapInput, + EmbeddableOutput, + MapEmbeddable +>(MapEmbeddableComponentInner); diff --git a/public/embeddable/map_embeddable.tsx b/public/embeddable/map_embeddable.tsx new file mode 100644 index 00000000..72e03d0b --- /dev/null +++ b/public/embeddable/map_embeddable.tsx @@ -0,0 +1,97 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import ReactDOM from 'react-dom'; +import { Subscription } from 'rxjs'; +import { MAP_SAVED_OBJECT_TYPE, MAPS_APP_ID } from '../../common'; +import { + Embeddable, + EmbeddableInput, + EmbeddableOutput, + IContainer, +} from '../../../../src/plugins/embeddable/public'; +import { MapEmbeddableComponent } from './map_component'; +import { ConfigSchema } from '../../common/config'; +import { MapSavedObjectAttributes } from '../../common/map_saved_object_attributes'; +import { RefreshInterval } from '../../../../src/plugins/data/public'; + +export const MAP_EMBEDDABLE = MAP_SAVED_OBJECT_TYPE; + +export interface MapInput extends EmbeddableInput { + savedObjectId: string; + refreshConfig?: RefreshInterval; +} + +export type MapOutput = EmbeddableOutput; + +function getOutput(input: MapInput, editUrl: string, tittle: string): MapOutput { + return { + editable: true, + editUrl, + defaultTitle: tittle, + editApp: MAPS_APP_ID, + editPath: input.savedObjectId, + }; +} + +export class MapEmbeddable extends Embeddable { + public readonly type = MAP_EMBEDDABLE; + private subscription: Subscription; + private node?: HTMLElement; + private readonly mapConfig: ConfigSchema; + private readonly services: any; + constructor( + initialInput: MapInput, + { + parent, + services, + mapConfig, + editUrl, + savedMapAttributes, + }: { + parent?: IContainer; + services: any; + mapConfig: ConfigSchema; + editUrl: string; + savedMapAttributes: MapSavedObjectAttributes; + } + ) { + super(initialInput, getOutput(initialInput, editUrl, savedMapAttributes.title), parent); + this.mapConfig = mapConfig; + this.services = services; + this.subscription = this.getInput$().subscribe(() => { + this.updateOutput(getOutput(this.input, editUrl, savedMapAttributes.title)); + }); + } + + public render(node: HTMLElement) { + this.node = node; + if (this.node) { + ReactDOM.unmountComponentAtNode(this.node); + } + ReactDOM.render(, node); + } + + public reload() { + if (this.node) { + this.render(this.node); + } + } + + public destroy() { + super.destroy(); + this.subscription.unsubscribe(); + if (this.node) { + ReactDOM.unmountComponentAtNode(this.node); + } + } + public getServiceSettings() { + return this.services; + } + public getMapConfig() { + return this.mapConfig; + } +} diff --git a/public/embeddable/map_embeddable_factory.tsx b/public/embeddable/map_embeddable_factory.tsx new file mode 100644 index 00000000..b1534687 --- /dev/null +++ b/public/embeddable/map_embeddable_factory.tsx @@ -0,0 +1,99 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { i18n } from '@osd/i18n'; +import { + IContainer, + EmbeddableFactoryDefinition, + ErrorEmbeddable, + SavedObjectEmbeddableInput, +} from '../../../../src/plugins/embeddable/public'; +import { MAP_EMBEDDABLE, MapInput, MapOutput, MapEmbeddable } from './map_embeddable'; +import { MAPS_APP_ICON, MAPS_APP_ID } from '../../common'; +import { ConfigSchema } from '../../common/config'; +import { MapSavedObjectAttributes } from '../../common/map_saved_object_attributes'; +import { MAPS_APP_DISPLAY_NAME } from '../../common/constants/shared'; +import { getTimeFilter } from '../services'; + +interface StartServices { + services: { + application: { + getUrlForApp: (appId: string, options?: { path?: string }) => string; + navigateToApp: (appId: string, options?: { path?: string }) => Promise; + }; + savedObjects: { + client: { + get: (type: string, id: string) => Promise; + }; + }; + }; + mapConfig: ConfigSchema; +} + +export class MapEmbeddableFactoryDefinition + implements EmbeddableFactoryDefinition +{ + public readonly type = MAP_EMBEDDABLE; + + public readonly savedObjectMetaData = { + name: MAPS_APP_DISPLAY_NAME, + type: MAP_EMBEDDABLE, + getIconForSavedObject: () => MAPS_APP_ICON, + }; + + constructor(private getStartServices: () => Promise) {} + + public async isEditable() { + return true; + } + + // Maps app will be created from visualization list + public canCreateNew() { + return false; + } + + public createFromSavedObject = async ( + savedObjectId: string, + input: Partial & { id: string }, + parent?: IContainer + ): Promise => { + try { + const { services, mapConfig } = await this.getStartServices(); + const url = services.application.getUrlForApp(MAPS_APP_ID, { + path: savedObjectId, + }); + const timeFilter = getTimeFilter(); + const savedMap = await services.savedObjects.client.get(MAP_EMBEDDABLE, savedObjectId); + const savedMapAttributes = savedMap.attributes as MapSavedObjectAttributes; + return new MapEmbeddable( + { + ...input, + savedObjectId, + title: savedMapAttributes.title, + }, + { + parent, + services, + mapConfig, + editUrl: url, + savedMapAttributes, + timeFilter, + } + ); + } catch (error) { + return new ErrorEmbeddable(error.message, input); + } + }; + + public async create(initialInput: MapInput, parent?: IContainer) { + return undefined; + } + + public getDisplayName() { + return i18n.translate('maps.displayName', { + defaultMessage: MAPS_APP_DISPLAY_NAME, + }); + } +} diff --git a/public/model/layerRenderController.ts b/public/model/layerRenderController.ts index 7c11bb2d..cf160535 100644 --- a/public/model/layerRenderController.ts +++ b/public/model/layerRenderController.ts @@ -14,11 +14,13 @@ import { getTime, IOpenSearchDashboardsSearchResponse, isCompleteResponse, + TimeRange, + Query, } from '../../../../src/plugins/data/common'; import { layersFunctionMap } from './layersFunctions'; import { MapServices } from '../types'; import { MapState } from './mapState'; -import {GeoBounds, getBounds} from './map/boundary'; +import { GeoBounds, getBounds } from './map/boundary'; import { buildBBoxFilter } from './geo/filter'; interface MaplibreRef { @@ -28,8 +30,10 @@ interface MaplibreRef { export const prepareDataLayerSource = ( layer: MapLayerSpecification, mapState: MapState, - { data, notifications }: MapServices, - filters: Filter[] = [] + { data, toastNotifications }: MapServices, + filters: Filter[] = [], + timeRange?: TimeRange, + query?: Query ): Promise => { return new Promise(async (resolve, reject) => { if (layer.type === DASHBOARDS_MAPS_LAYER_TYPE.DOCUMENTS) { @@ -42,17 +46,19 @@ export const prepareDataLayerSource = ( sourceFields.push(...sourceConfig.tooltipFields); } let buildQuery; + let selectedTimeRange; if (indexPattern) { - const timeFilters = getTime(indexPattern, mapState.timeRange); - buildQuery = buildOpenSearchQuery( - indexPattern, - [], - [ - ...filters, - ...(layer.source.filters ? layer.source.filters : []), - ...(timeFilters ? [timeFilters] : []), - ] - ); + if (timeRange) { + selectedTimeRange = timeRange; + } else { + selectedTimeRange = mapState.timeRange; + } + const timeFilters = getTime(indexPattern, selectedTimeRange); + buildQuery = buildOpenSearchQuery(indexPattern, query ? [query] : [], [ + ...filters, + ...(layer.source.filters ? layer.source.filters : []), + ...(timeFilters ? [timeFilters] : []), + ]); } const request = { params: { @@ -72,7 +78,7 @@ export const prepareDataLayerSource = ( search$.unsubscribe(); resolve({ dataSource, layer }); } else { - notifications.toasts.addWarning('An error has occurred when query dataSource'); + toastNotifications.addWarning('An error has occurred when query dataSource'); search$.unsubscribe(); reject(); } @@ -90,9 +96,14 @@ export const handleDataLayerRender = ( mapState: MapState, services: MapServices, maplibreRef: MaplibreRef, - beforeLayerId: string | undefined + beforeLayerId: string | undefined, + timeRange?: TimeRange, + filtersFromDashboard?: Filter[], + query?: Query ) => { + // filters are passed from dashboard filters and geo bounding box filters const filters: Filter[] = []; + filters.push(...(filtersFromDashboard ? filtersFromDashboard : [])); const geoField = mapLayer.source.geoFieldName; const geoFieldType = mapLayer.source.geoFieldType; @@ -106,12 +117,14 @@ export const handleDataLayerRender = ( const geoBoundingBoxFilter: GeoBoundingBoxFilter = buildBBoxFilter(geoField, mapBounds, meta); filters.push(geoBoundingBoxFilter); - return prepareDataLayerSource(mapLayer, mapState, services, filters).then((result) => { - const { layer, dataSource } = result; - if (layer.type === DASHBOARDS_MAPS_LAYER_TYPE.DOCUMENTS) { - layersFunctionMap[layer.type].render(maplibreRef, layer, dataSource, beforeLayerId); + return prepareDataLayerSource(mapLayer, mapState, services, filters, timeRange, query).then( + (result) => { + const { layer, dataSource } = result; + if (layer.type === DASHBOARDS_MAPS_LAYER_TYPE.DOCUMENTS) { + layersFunctionMap[layer.type].render(maplibreRef, layer, dataSource, beforeLayerId); + } } - }); + ); }; export const handleReferenceLayerRender = ( diff --git a/public/plugin.tsx b/public/plugin.tsx index ee892f07..ce777fdd 100644 --- a/public/plugin.tsx +++ b/public/plugin.tsx @@ -19,32 +19,42 @@ import { } from './types'; import { PLUGIN_NAME, - PLUGIN_NAVIGATION_BAR_ID, - PLUGIN_NAVIGATION_BAR_TILE, + MAPS_APP_ID, + MAPS_APP_DISPLAY_NAME, + PLUGIN_ID, } from '../common/constants/shared'; import { ConfigSchema } from '../common/config'; import { AppPluginSetupDependencies } from './types'; import { RegionMapVisualizationDependencies } from '../../../src/plugins/region_map/public'; import { VectorUploadOptions } from './components/vector_upload_options'; import { OpenSearchDashboardsContextProvider } from '../../../src/plugins/opensearch_dashboards_react/public'; +import { + MAPS_APP_ICON, + MAP_SAVED_OBJECT_TYPE, + APP_PATH, + MAPS_VISUALIZATION_DESCRIPTION, +} from '../common'; +import { MapEmbeddableFactoryDefinition } from './embeddable'; +import { setTimeFilter } from './services'; export class CustomImportMapPlugin - implements Plugin { + implements Plugin +{ readonly _initializerContext: PluginInitializerContext; constructor(initializerContext: PluginInitializerContext) { this._initializerContext = initializerContext; } public setup( core: CoreSetup, - { regionMap }: AppPluginSetupDependencies + { regionMap, embeddable, visualizations }: AppPluginSetupDependencies ): CustomImportMapPluginSetup { const mapConfig: ConfigSchema = { ...this._initializerContext.config.get(), }; // Register an application into the side navigation menu core.application.register({ - id: PLUGIN_NAVIGATION_BAR_ID, - title: PLUGIN_NAVIGATION_BAR_TILE, + id: MAPS_APP_ID, + title: MAPS_APP_DISPLAY_NAME, order: 5100, category: { id: 'opensearch', @@ -56,7 +66,11 @@ export class CustomImportMapPlugin const { renderApp } = await import('./application'); // Get start services as specified in opensearch_dashboards.json const [coreStart, depsStart] = await core.getStartServices(); - const { navigation, data } = depsStart as AppPluginStartDependencies; + const { + navigation, + data, + embeddable: useEmbeddable, + } = depsStart as AppPluginStartDependencies; // make sure the index pattern list is up-to-date data.indexPatterns.clearCache(); @@ -73,12 +87,57 @@ export class CustomImportMapPlugin toastNotifications: coreStart.notifications.toasts, history: params.history, data, + embeddable: useEmbeddable, + scopedHistory: params.history, }; + params.element.classList.add('mapAppContainer'); // Render the application return renderApp(params, services, mapConfig); }, }); + const mapEmbeddableFactory = new MapEmbeddableFactoryDefinition(async () => { + const [coreStart, depsStart] = await core.getStartServices(); + const { navigation, data: useData } = depsStart as AppPluginStartDependencies; + return { + mapConfig, + services: { + ...coreStart, + navigation, + data: useData, + toastNotifications: coreStart.notifications.toasts, + }, + }; + }); + embeddable.registerEmbeddableFactory(MAP_SAVED_OBJECT_TYPE, mapEmbeddableFactory as any); + + visualizations.registerAlias({ + name: PLUGIN_ID, + title: MAPS_APP_DISPLAY_NAME, + description: MAPS_VISUALIZATION_DESCRIPTION, + icon: MAPS_APP_ICON, + aliasApp: MAPS_APP_ID, + aliasPath: APP_PATH.CREATE_MAP, + stage: 'production', + appExtensions: { + visualizations: { + docTypes: [MAP_SAVED_OBJECT_TYPE], + toListItem: ({ id, attributes, updated_at: updatedAt }) => ({ + description: attributes?.description, + editApp: MAPS_APP_ID, + editUrl: `${encodeURIComponent(id)}`, + icon: MAPS_APP_ICON, + id, + savedObjectType: MAP_SAVED_OBJECT_TYPE, + title: attributes?.title, + typeTitle: MAPS_APP_DISPLAY_NAME, + stage: 'production', + updated_at: updatedAt, + }), + }, + }, + }); + const customSetup = async () => { const [coreStart] = await core.getStartServices(); regionMap.addOptionTab({ @@ -108,7 +167,8 @@ export class CustomImportMapPlugin }; } - public start(core: CoreStart): CustomImportMapPluginStart { + public start(core: CoreStart, { data }: AppPluginStartDependencies): CustomImportMapPluginStart { + setTimeFilter(data.query.timefilter.timefilter); return {}; } diff --git a/public/services.ts b/public/services.ts index 25166d39..da4ce07b 100644 --- a/public/services.ts +++ b/public/services.ts @@ -4,6 +4,8 @@ */ import { CoreStart } from '../../../src/core/public'; +import { createGetterSetter } from '../../../src/plugins/opensearch_dashboards_utils/common'; +import { TimefilterContract } from '../../../src/plugins/data/public'; export const postGeojson = async (requestBody: any, http: CoreStart['http']) => { try { @@ -28,3 +30,5 @@ export const getIndex = async (indexName: string, http: CoreStart['http']) => { return e; } }; + +export const [getTimeFilter, setTimeFilter] = createGetterSetter('TimeFilter'); diff --git a/public/types.ts b/public/types.ts index d34f7273..657d6c4c 100644 --- a/public/types.ts +++ b/public/types.ts @@ -8,16 +8,19 @@ import { CoreStart, SavedObjectsClient, ToastsStart, -} from 'opensearch-dashboards/public'; + ScopedHistory, +} from '../../../src/core/public'; import { NavigationPublicPluginStart } from '../../../src/plugins/navigation/public'; -import { DataPublicPluginStart } from '../../../src/plugins/data/public'; - +import { DataPublicPluginSetup, DataPublicPluginStart } from '../../../src/plugins/data/public'; import { RegionMapPluginSetup } from '../../../src/plugins/region_map/public'; +import { EmbeddableSetup, EmbeddableStart } from '../../../src/plugins/embeddable/public'; +import { VisualizationsSetup } from '../../../src/plugins/visualizations/public'; export interface AppPluginStartDependencies { navigation: NavigationPublicPluginStart; savedObjects: SavedObjectsClient; data: DataPublicPluginStart; + embeddable: EmbeddableStart; } export interface MapServices extends CoreStart { @@ -28,20 +31,24 @@ export interface MapServices extends CoreStart { toastNotifications: ToastsStart; history: AppMountParameters['history']; data: DataPublicPluginStart; + application: CoreStart['application']; + i18n: CoreStart['i18n']; + savedObjects: CoreStart['savedObjects']; + overlays: CoreStart['overlays']; + embeddable: EmbeddableStart; + scopedHistory: ScopedHistory; + chrome: CoreStart['chrome']; } // eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface CustomImportMapPluginSetup { - getGreeting: () => string; -} +export interface CustomImportMapPluginSetup {} // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface CustomImportMapPluginStart {} -export interface AppPluginStartDependencies { - navigation: NavigationPublicPluginStart; -} - export interface AppPluginSetupDependencies { regionMap: RegionMapPluginSetup; + embeddable: EmbeddableSetup; + visualizations: VisualizationsSetup; + data: DataPublicPluginSetup; } diff --git a/public/utils/breadcrumbs.ts b/public/utils/breadcrumbs.ts index daa69ea2..40308ce0 100644 --- a/public/utils/breadcrumbs.ts +++ b/public/utils/breadcrumbs.ts @@ -4,7 +4,7 @@ */ import { i18n } from '@osd/i18n'; -import {PLUGIN_NAVIGATION_BAR_ID} from '../../common'; +import { MAPS_APP_ID } from '../../common'; export function getMapsLandingBreadcrumbs(navigateToApp: any) { return [ @@ -12,7 +12,7 @@ export function getMapsLandingBreadcrumbs(navigateToApp: any) { text: i18n.translate('maps.listing.breadcrumb', { defaultMessage: 'Maps', }), - onClick: () => navigateToApp(PLUGIN_NAVIGATION_BAR_ID), + onClick: () => navigateToApp(MAPS_APP_ID), }, ]; } diff --git a/yarn.lock b/yarn.lock index f21443e3..7b5d9661 100644 --- a/yarn.lock +++ b/yarn.lock @@ -127,6 +127,32 @@ resolved "https://registry.yarnpkg.com/@types/pbf/-/pbf-3.0.2.tgz#8d291ad68b4b8c533e96c174a2e3e6399a59ed61" integrity sha512-EDrLIPaPXOZqDjrkzxxbX7UlJSeQVgah3i0aA4pOSzmK9zq3BIh7/MZIQxED7slJByvKM4Gc6Hypyu2lJzh3SQ== +"@types/prop-types@*": + version "15.7.5" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf" + integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w== + +"@types/react-test-renderer@^18.0.0": + version "18.0.0" + resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-18.0.0.tgz#7b7f69ca98821ea5501b21ba24ea7b6139da2243" + integrity sha512-C7/5FBJ3g3sqUahguGi03O79b8afNeSD6T8/GU50oQrJCU0bVCCGQHaGKUbg2Ce8VQEEqTw8/HiS6lXHHdgkdQ== + dependencies: + "@types/react" "*" + +"@types/react@*": + version "18.0.28" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.28.tgz#accaeb8b86f4908057ad629a26635fe641480065" + integrity sha512-RD0ivG1kEztNBdoAK7lekI9M+azSnitIn85h4iOiaLjaTrMjzslhaqCGaI4IyCJ1RljWiLCEu4jyrLLgqxBTew== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" + +"@types/scheduler@*": + version "0.16.2" + resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39" + integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew== + "@types/sinonjs__fake-timers@8.1.1": version "8.1.1" resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz#b49c2c70150141a15e0fa7e79cf1f92a72934ce3" @@ -405,6 +431,11 @@ csscolorparser@~1.0.3: resolved "https://registry.yarnpkg.com/csscolorparser/-/csscolorparser-1.0.3.tgz#b34f391eea4da8f3e98231e2ccd8df9c041f171b" integrity sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w== +csstype@^3.0.2: + version "3.1.1" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.1.tgz#841b532c45c758ee546a11d5bd7b7b473c8c30b9" + integrity sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw== + cypress-file-upload@^5.0.8: version "5.0.8" resolved "https://registry.yarnpkg.com/cypress-file-upload/-/cypress-file-upload-5.0.8.tgz#d8824cbeaab798e44be8009769f9a6c9daa1b4a1"