From ec8778926c464b770cedb0018e206e8dddc7440b Mon Sep 17 00:00:00 2001 From: Junqiu Lei Date: Fri, 19 Apr 2024 13:35:08 -0700 Subject: [PATCH] Support multi data source display in Maps app (#611) * Support multi data source display in Maps app Signed-off-by: Junqiu Lei (cherry picked from commit f325212eb75c491b1b9e715403732d810197c869) Signed-off-by: Junqiu Lei --- CHANGELOG.md | 1 + opensearch_dashboards.json | 6 +- public/application.tsx | 6 +- public/components/app.tsx | 6 +- .../layer_control_panel.tsx | 33 ++++++++- .../map_container/map_container.tsx | 14 ++++ public/components/map_page/map_page.tsx | 70 +++++++++++++------ .../components/map_top_nav/top_nav_menu.tsx | 60 ++++++++++------ public/components/maps_list/maps_list.tsx | 28 ++++++-- public/plugin.tsx | 10 +-- public/types.ts | 5 ++ yarn.lock | 28 ++++---- 12 files changed, 197 insertions(+), 70 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 287942cf..d9a012a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased 2.x](https://github.com/opensearch-project/dashboards-maps/compare/2.13...2.x) ### Features +Support multi data source display in Maps app([#611](https://github.com/opensearch-project/dashboards-maps/pull/611)) ### Enhancements ### Bug Fixes * Fix zoom level type error in custom layer ([#605](https://github.com/opensearch-project/dashboards-maps/pull/605)) diff --git a/opensearch_dashboards.json b/opensearch_dashboards.json index 874ff231..2287c63a 100644 --- a/opensearch_dashboards.json +++ b/opensearch_dashboards.json @@ -15,6 +15,8 @@ "visualizations" ], "optionalPlugins": [ - "home" + "home", + "dataSource", + "dataSourceManagement" ] -} \ No newline at end of file +} diff --git a/public/application.tsx b/public/application.tsx index aab8c3ea..9fec4afe 100644 --- a/public/application.tsx +++ b/public/application.tsx @@ -10,7 +10,11 @@ import { MapServices } from './types'; import { MapsDashboardsApp } from './components/app'; import { OpenSearchDashboardsContextProvider } from '../../../src/plugins/opensearch_dashboards_react/public'; -export const renderApp = ({ element }: AppMountParameters, services: MapServices) => { +export const renderApp = ( + { element }: AppMountParameters, + services: MapServices, + dataSourceManagementEnabled: boolean +) => { ReactDOM.render( diff --git a/public/components/app.tsx b/public/components/app.tsx index ac725b24..f8d1b4a1 100644 --- a/public/components/app.tsx +++ b/public/components/app.tsx @@ -22,7 +22,11 @@ export const MapsDashboardsApp = () => { } /> - } /> + } + /> diff --git a/public/components/layer_control_panel/layer_control_panel.tsx b/public/components/layer_control_panel/layer_control_panel.tsx index 993fc0b2..44be9bd9 100644 --- a/public/components/layer_control_panel/layer_control_panel.tsx +++ b/public/components/layer_control_panel/layer_control_panel.tsx @@ -29,7 +29,7 @@ import { IndexPattern } from '../../../../../src/plugins/data/public'; import { AddLayerPanel } from '../add_layer_panel'; import { LayerConfigPanel } from '../layer_config'; import { MapLayerSpecification } from '../../model/mapLayerType'; -import { LAYER_ICON_TYPE_MAP } from '../../../common'; +import { DASHBOARDS_MAPS_LAYER_TYPE, LAYER_ICON_TYPE_MAP } from '../../../common'; import { useOpenSearchDashboards } from '../../../../../src/plugins/opensearch_dashboards_react/public'; import { MapServices } from '../../types'; import { MapState } from '../../model/mapState'; @@ -49,6 +49,7 @@ interface Props { selectedLayerConfig: MapLayerSpecification | undefined; setSelectedLayerConfig: (layerConfig: MapLayerSpecification | undefined) => void; setIsUpdatingLayerRender: (isUpdatingLayerRender: boolean) => void; + setDataSourceRefIds: (dataSourceRefIds: string[]) => void; } export const LayerControlPanel = memo( @@ -56,11 +57,14 @@ export const LayerControlPanel = memo( maplibreRef, setLayers, layers, + layersIndexPatterns, + setLayersIndexPatterns, zoom, isReadOnlyMode, selectedLayerConfig, setSelectedLayerConfig, setIsUpdatingLayerRender, + setDataSourceRefIds, }: Props) => { const { services } = useOpenSearchDashboards(); @@ -205,12 +209,39 @@ export const LayerControlPanel = memo( setIsDeleteLayerModalVisible(true); }; + const removeDataLayerDataSource = (layer: MapLayerSpecification) => { + if (layer.type === DASHBOARDS_MAPS_LAYER_TYPE.DOCUMENTS) { + const indexPatternId = layer.source.indexPatternId; + const indexPattern = layersIndexPatterns.find((idp) => idp.id === indexPatternId); + if (indexPattern) { + const indexPatternClone = [...layersIndexPatterns]; + const index = indexPatternClone.findIndex((idp) => idp.id === indexPatternId); + if (index > -1) { + indexPatternClone.splice(index, 1); + setLayersIndexPatterns(indexPatternClone); + } + // remove duplicate dataSourceRefIds + const updatedDataSourceRefIds : string[] = []; + indexPatternClone.forEach((ip) => { + if (ip.dataSourceRef && !updatedDataSourceRefIds.includes(ip.dataSourceRef.id)) { + updatedDataSourceRefIds.push(ip.dataSourceRef.id); + } else if (!ip.dataSourceRef && !updatedDataSourceRefIds.includes('')) { + updatedDataSourceRefIds.push(''); + } + }); + + setDataSourceRefIds(updatedDataSourceRefIds); + } + } + }; + const onDeleteLayerConfirm = () => { if (selectedDeleteLayer) { removeLayers(maplibreRef.current!, selectedDeleteLayer.id, true); removeLayer(selectedDeleteLayer.id); setIsDeleteLayerModalVisible(false); setSelectedDeleteLayer(undefined); + removeDataLayerDataSource(selectedDeleteLayer); } }; diff --git a/public/components/map_container/map_container.tsx b/public/components/map_container/map_container.tsx index 8fc7f120..e819ddfa 100644 --- a/public/components/map_container/map_container.tsx +++ b/public/components/map_container/map_container.tsx @@ -46,6 +46,8 @@ interface MapContainerProps { isUpdatingLayerRender: boolean; setIsUpdatingLayerRender: (isUpdatingLayerRender: boolean) => void; addSpatialFilter: (shape: ShapeFilter, label: string | null, relation: GeoShapeRelation) => void; + dataSourceRefIds: string[]; + setDataSourceRefIds: (refIds: string[]) => void; } export class MapsServiceError extends Error { @@ -67,6 +69,7 @@ export const MapContainer = ({ isUpdatingLayerRender, setIsUpdatingLayerRender, addSpatialFilter, + setDataSourceRefIds, }: MapContainerProps) => { const { services } = useOpenSearchDashboards(); @@ -245,6 +248,16 @@ export const MapContainer = ({ ); const cloneLayersIndexPatterns = [...layersIndexPatterns, newIndexPattern]; setLayersIndexPatterns(cloneLayersIndexPatterns); + const updatedDataSourceRefIds: string[] = []; + cloneLayersIndexPatterns.forEach((ip) => { + if (ip.dataSourceRef && !updatedDataSourceRefIds.includes(ip.dataSourceRef.id)) { + updatedDataSourceRefIds.push(ip.dataSourceRef.id); + } else if (!ip.dataSourceRef && !updatedDataSourceRefIds.includes('')) { + // If index pattern of the layer doesn't have reference to a data source, it is using local cluster + updatedDataSourceRefIds.push(''); + } + }); + setDataSourceRefIds(updatedDataSourceRefIds); } }; @@ -271,6 +284,7 @@ export const MapContainer = ({ selectedLayerConfig={selectedLayerConfig} setSelectedLayerConfig={setSelectedLayerConfig} setIsUpdatingLayerRender={setIsUpdatingLayerRender} + setDataSourceRefIds={setDataSourceRefIds} /> )} {mounted && tooltipState === TOOLTIP_STATE.DISPLAY_FEATURES && maplibreRef.current && ( diff --git a/public/components/map_page/map_page.tsx b/public/components/map_page/map_page.tsx index 8f3310fe..997c8806 100644 --- a/public/components/map_page/map_page.tsx +++ b/public/components/map_page/map_page.tsx @@ -51,13 +51,16 @@ export const MapComponent = ({ mapIdFromSavedObject, dashboardProps }: MapCompon savedObjects: { client: savedObjectsClient }, } = services; const [layers, setLayers] = useState([]); - const [savedMapObject, setSavedMapObject] = - useState | null>(); + const [savedMapObject, setSavedMapObject] = useState | null>(); const [layersIndexPatterns, setLayersIndexPatterns] = useState([]); const maplibreRef = useRef(null); const [mapState, setMapState] = useState(getInitialMapState()); const [isUpdatingLayerRender, setIsUpdatingLayerRender] = useState(true); const isReadOnlyMode = !!dashboardProps; + const [dataSourceRefIds, setDataSourceRefIds] = useState([]); + const [dataLoadReady, setDataLoadReady] = useState(false); useEffect(() => { if (mapIdFromSavedObject) { @@ -68,14 +71,31 @@ export const MapComponent = ({ mapIdFromSavedObject, dashboardProps }: MapCompon setMapState(savedMapState); setLayers(layerList); const savedIndexPatterns: IndexPattern[] = []; - layerList.forEach(async (layer: MapLayerSpecification) => { - if (layer.type === DASHBOARDS_MAPS_LAYER_TYPE.DOCUMENTS) { - const indexPatternId = layer.source.indexPatternId; - const indexPattern = await services.data.indexPatterns.get(indexPatternId); - savedIndexPatterns.push(indexPattern); - } - }); - setLayersIndexPatterns(savedIndexPatterns); + const remoteDataSourceIds: string[] = []; + + const fetchDataLayer = async () => { + const requests = layerList + .filter((layer) => layer.type === DASHBOARDS_MAPS_LAYER_TYPE.DOCUMENTS) + .map((layer) => services.data.indexPatterns.get(layer.source.indexPatternId)); + + const resp = await Promise.all(requests); + resp.forEach((response: IndexPattern) => { + savedIndexPatterns.push(response); + if (response.dataSourceRef && !dataSourceRefIds.includes(response.dataSourceRef.id)) { + remoteDataSourceIds.push(response.dataSourceRef.id); + } else if (!response.dataSourceRef && !remoteDataSourceIds.includes('')) { + // If index pattern of the layer doesn't have reference to a data source, it is using local cluster + remoteDataSourceIds.push(''); + } + }); + + setLayers(layerList); + setLayersIndexPatterns(savedIndexPatterns); + setDataSourceRefIds(remoteDataSourceIds); + setDataLoadReady(true); + }; + + fetchDataLayer(); }); } else { const initialDefaultLayer: MapLayerSpecification = getLayerConfigMap()[ @@ -83,6 +103,7 @@ export const MapComponent = ({ mapIdFromSavedObject, dashboardProps }: MapCompon ] as MapLayerSpecification; initialDefaultLayer.name = MAP_LAYER_DEFAULT_NAME; setLayers([initialDefaultLayer]); + setDataLoadReady(true); } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -114,18 +135,21 @@ export const MapComponent = ({ mapIdFromSavedObject, dashboardProps }: MapCompon return (
- {isReadOnlyMode ? null : ( - - )} + {isReadOnlyMode + ? null + : dataLoadReady && ( + + )} {!isReadOnlyMode && !!mapState.spatialMetaFilters?.length && (
@@ -149,6 +173,8 @@ export const MapComponent = ({ mapIdFromSavedObject, dashboardProps }: MapCompon isUpdatingLayerRender={isUpdatingLayerRender} setIsUpdatingLayerRender={setIsUpdatingLayerRender} addSpatialFilter={addSpatialFilter} + dataSourceRefIds={dataSourceRefIds} + setDataSourceRefIds={setDataSourceRefIds} />
); diff --git a/public/components/map_top_nav/top_nav_menu.tsx b/public/components/map_top_nav/top_nav_menu.tsx index d6519d94..3fd13f20 100644 --- a/public/components/map_top_nav/top_nav_menu.tsx +++ b/public/components/map_top_nav/top_nav_menu.tsx @@ -26,6 +26,7 @@ interface MapTopNavMenuProps { setMapState: (mapState: MapState) => void; originatingApp?: string; setIsUpdatingLayerRender: (isUpdatingLayerRender: boolean) => void; + dataSourceRefIds: string[]; } export const MapTopNavMenu = ({ @@ -37,6 +38,7 @@ export const MapTopNavMenu = ({ mapState, setMapState, setIsUpdatingLayerRender, + dataSourceRefIds, }: MapTopNavMenuProps) => { const { services } = useOpenSearchDashboards(); const { @@ -48,6 +50,9 @@ export const MapTopNavMenu = ({ application: { navigateToApp }, embeddable, scopedHistory, + dataSourceManagement, + savedObjects: { client: savedObjectsClient }, + notifications, } = services; const [title, setTitle] = useState(''); @@ -132,27 +137,42 @@ export const MapTopNavMenu = ({ }); }, [services, mapIdFromUrl, layers, title, description, mapState, originatingApp]); + const dataSourceManagementEnabled: boolean = !!dataSourceManagement; + return ( // @ts-ignore - + <> + + ); }; diff --git a/public/components/maps_list/maps_list.tsx b/public/components/maps_list/maps_list.tsx index 78ce10fd..7cc57d06 100644 --- a/public/components/maps_list/maps_list.tsx +++ b/public/components/maps_list/maps_list.tsx @@ -22,14 +22,17 @@ import { MapSavedObjectAttributes } from '../../../common/map_saved_object_attri import { MapServices } from '../../types'; import { getMapsLandingBreadcrumbs } from '../../utils/breadcrumbs'; import { APP_PATH, MAPS_APP_ID } from '../../../common'; +import { DataSourceAggregatedViewConfig } from '../../../../../src/plugins/data_source_management/public'; export const MapsList = () => { const { services: { - notifications: { toasts }, + notifications, savedObjects: { client: savedObjectsClient }, application: { navigateToApp }, chrome: { docTitle, setBreadcrumbs }, + dataSourceManagement, + setActionMenu, }, } = useOpenSearchDashboards(); @@ -92,14 +95,14 @@ export const MapsList = () => { await Promise.all( selectedItems.map((item: any) => savedObjectsClient.delete(item.type, item.id)) ).catch((error) => { - toasts.addError(error, { + notifications.toasts.addError(error, { title: i18n.translate('map.mapListingDeleteErrorTitle', { defaultMessage: 'Error deleting map', }), }); }); }, - [savedObjectsClient, toasts] + [savedObjectsClient, notifications.toasts] ); const noMapItem = ( @@ -113,14 +116,29 @@ export const MapsList = () => { ]} /> ); + const dataSourceManagementEnabled: boolean = !!dataSourceManagement; - // Render the map list DOM. return ( <> + {dataSourceManagementEnabled && (() => { + const DataSourcesMenu = dataSourceManagement.ui.getDataSourceMenu(); + return ( + + ); + })()} { tableListTitle={i18n.translate('maps.listing.table.listTitle', { defaultMessage: 'Maps', })} - toastNotifications={toasts} + toastNotifications={notifications.toasts} /> diff --git a/public/plugin.tsx b/public/plugin.tsx index a08c698e..66e2b085 100644 --- a/public/plugin.tsx +++ b/public/plugin.tsx @@ -38,19 +38,19 @@ 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, embeddable, visualizations }: AppPluginSetupDependencies + { regionMap, embeddable, visualizations, dataSourceManagement }: AppPluginSetupDependencies ): CustomImportMapPluginSetup { const mapConfig: ConfigSchema = { ...this._initializerContext.config.get(), }; + const dataSourceManagentEnabled: boolean = !!dataSourceManagement; // Register an application into the side navigation menu core.application.register({ id: MAPS_APP_ID, @@ -91,10 +91,12 @@ export class CustomImportMapPlugin scopedHistory: params.history, uiSettings: coreStart.uiSettings, mapConfig, + dataSourceManagement, + setActionMenu: params.setHeaderActionMenu, }; params.element.classList.add('mapAppContainer'); // Render the application - return renderApp(params, services); + return renderApp(params, services, dataSourceManagentEnabled); }, }); diff --git a/public/types.ts b/public/types.ts index 8d57c75a..8376ae84 100644 --- a/public/types.ts +++ b/public/types.ts @@ -9,6 +9,7 @@ import { SavedObjectsClient, ToastsStart, ScopedHistory, + MountPoint, } from '../../../src/core/public'; import { NavigationPublicPluginStart } from '../../../src/plugins/navigation/public'; import { DataPublicPluginSetup, DataPublicPluginStart } from '../../../src/plugins/data/public'; @@ -16,6 +17,7 @@ import { RegionMapPluginSetup } from '../../../src/plugins/region_map/public'; import { EmbeddableSetup, EmbeddableStart } from '../../../src/plugins/embeddable/public'; import { VisualizationsSetup } from '../../../src/plugins/visualizations/public'; import { ConfigSchema } from '../common/config'; +import { DataSourceManagementPluginSetup } from '../../../src/plugins/data_source_management/public'; export interface AppPluginStartDependencies { navigation: NavigationPublicPluginStart; @@ -41,6 +43,8 @@ export interface MapServices extends CoreStart { chrome: CoreStart['chrome']; uiSettings: CoreStart['uiSettings']; mapConfig: ConfigSchema; + dataSourceManagement: DataSourceManagementPluginSetup; + setActionMenu: (menuMount: MountPoint | undefined) => void; } // eslint-disable-next-line @typescript-eslint/no-empty-interface @@ -54,4 +58,5 @@ export interface AppPluginSetupDependencies { embeddable: EmbeddableSetup; visualizations: VisualizationsSetup; data: DataPublicPluginSetup; + dataSourceManagement: DataSourceManagementPluginSetup; } diff --git a/yarn.lock b/yarn.lock index f2c5d089..b811fbcb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8,9 +8,9 @@ integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== "@cypress/request@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@cypress/request/-/request-3.0.0.tgz#7f58dfda087615ed4e6aab1b25fffe7630d6dd85" - integrity sha512-GKFCqwZwMYmL3IBoNeR2MM1SnxRIGERsQOTWeQKoYBt2JLqcqiy7JXqO894FLrpjZYqGxW92MNwRH2BN56obdQ== + version "3.0.1" + resolved "https://registry.yarnpkg.com/@cypress/request/-/request-3.0.1.tgz#72d7d5425236a2413bd3d8bb66d02d9dc3168960" + integrity sha512-TWivJlJi8ZDx2wGOw1dbLuHJKUYX7bWySw377nlnGOW3hP9/MUKIsEdXT/YngWxVdgNCHRBmFlBipE+5/2ZZlQ== dependencies: aws-sign2 "~0.7.0" aws4 "^1.8.0" @@ -25,7 +25,7 @@ json-stringify-safe "~5.0.1" mime-types "~2.1.19" performance-now "^2.1.0" - qs "~6.10.3" + qs "6.10.4" safe-buffer "^5.1.2" tough-cookie "^4.1.3" tunnel-agent "^0.6.0" @@ -516,9 +516,9 @@ cypress-multi-reporters@^1.5.0: lodash "^4.17.21" cypress@^13.6.3: - version "13.6.6" - resolved "https://registry.yarnpkg.com/cypress/-/cypress-13.6.6.tgz#5133f231ed1c6e57dc8dcbf60aade220bcd6884b" - integrity sha512-S+2S9S94611hXimH9a3EAYt81QM913ZVA03pUmGDfLTFa5gyp85NJ8dJGSlEAEmyRsYkioS1TtnWtbv/Fzt11A== + version "13.7.3" + resolved "https://registry.yarnpkg.com/cypress/-/cypress-13.7.3.tgz#3e7dcd32e007676a6c8e972293c50d6ef329d991" + integrity sha512-uoecY6FTCAuIEqLUYkTrxamDBjMHTYak/1O7jtgwboHiTnS1NaMOoR08KcTrbRZFCBvYOiS4tEkQRmsV+xcrag== dependencies: "@cypress/request" "^3.0.0" "@cypress/xvfb" "^1.2.4" @@ -1262,10 +1262,10 @@ punycode@^2.1.1: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f" integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA== -qs@~6.10.3: - version "6.10.5" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.5.tgz#974715920a80ff6a262264acd2c7e6c2a53282b4" - integrity sha512-O5RlPh0VFtR78y79rgcgKK4wbAI0C5zGVLztOIdpWX6ep368q5Hv6XRxDvXuZ9q3C6v+e3n8UfZZJw7IIG27eQ== +qs@6.10.4: + version "6.10.4" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.4.tgz#6a3003755add91c0ec9eacdc5f878b034e73f9e7" + integrity sha512-OQiU+C+Ds5qiH91qh/mg0w+8nwQuLjM4F4M/PbmhDOoYehPh+Fb0bDjtR1sOvy7YKxvj28Y/M0PhP5uVX0kB+g== dependencies: side-channel "^1.0.4" @@ -1353,9 +1353,9 @@ safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== semver@^7.5.3: - version "7.5.4" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" - integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== + version "7.6.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.0.tgz#1a46a4db4bffcccd97b743b5005c8325f23d4e2d" + integrity sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg== dependencies: lru-cache "^6.0.0"