diff --git a/common/index.ts b/common/index.ts index 5fc0986e..7a1dff36 100644 --- a/common/index.ts +++ b/common/index.ts @@ -46,7 +46,8 @@ 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 = 'globe'; +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/opensearch_dashboards.json b/opensearch_dashboards.json index 4ad5ffea..7e13e67c 100644 --- a/opensearch_dashboards.json +++ b/opensearch_dashboards.json @@ -4,6 +4,6 @@ "opensearchDashboardsVersion": "3.0.0", "server": true, "ui": true, - "requiredPlugins": ["regionMap", "opensearchDashboardsReact", "opensearchDashboardsUtils", "navigation", "savedObjects", "data", "embeddable"], + "requiredPlugins": ["regionMap", "opensearchDashboardsReact", "opensearchDashboardsUtils", "navigation", "savedObjects", "data", "embeddable", "visualizations"], "optionalPlugins": [] } diff --git a/public/components/add_layer_panel/add_layer_panel.tsx b/public/components/add_layer_panel/add_layer_panel.tsx index 59838467..c06aa078 100644 --- a/public/components/add_layer_panel/add_layer_panel.tsx +++ b/public/components/add_layer_panel/add_layer_panel.tsx @@ -41,7 +41,6 @@ interface Props { newLayerIndex: number; mapConfig: ConfigSchema; layerCount: number; - inDashboardMode: boolean; } export const AddLayerPanel = ({ @@ -53,7 +52,6 @@ export const AddLayerPanel = ({ newLayerIndex, mapConfig, layerCount, - inDashboardMode, }: Props) => { const [isAddNewLayerModalVisible, setIsAddNewLayerModalVisible] = useState(false); const [highlightItem, setHighlightItem] = useState(null); @@ -111,33 +109,31 @@ export const AddLayerPanel = ({ return (
- {inDashboardMode ? null : ( - - - {isMaxLayerLimitReached() - ? `You've added the maximum number of layers (${MAX_LAYER_LIMIT}).` - : 'Add layer'} -

- } + + + {isMaxLayerLimitReached() + ? `You've added the maximum number of layers (${MAX_LAYER_LIMIT}).` + : 'Add layer'} +

+ } + > + - - Add layer - -
-
- )} + Add layer + +
+
{isAddNewLayerModalVisible && ( diff --git a/public/components/layer_control_panel/layer_control_panel.tsx b/public/components/layer_control_panel/layer_control_panel.tsx index 4c5c25db..51643d83 100644 --- a/public/components/layer_control_panel/layer_control_panel.tsx +++ b/public/components/layer_control_panel/layer_control_panel.tsx @@ -30,7 +30,6 @@ import { AddLayerPanel } from '../add_layer_panel'; import { LayerConfigPanel } from '../layer_config'; import { MapLayerSpecification } from '../../model/mapLayerType'; import { - DASHBOARDS_MAPS_LAYER_TYPE, LAYER_ICON_TYPE_MAP, LAYER_PANEL_HIDE_LAYER_ICON, LAYER_PANEL_SHOW_LAYER_ICON, @@ -98,16 +97,17 @@ export const LayerControlPanel = memo( >(); const [visibleLayers, setVisibleLayers] = useState([]); + // Update data layers when state bar changes useEffect(() => { - if (timeRange) { - layers.forEach((layer: MapLayerSpecification) => { - if (layer.type === DASHBOARDS_MAPS_LAYER_TYPE.OPENSEARCH_MAP) { - return; - } - handleDataLayerRender(layer, mapState, services, maplibreRef, undefined, timeRange); - }); - } + layers.forEach((layer: MapLayerSpecification) => { + if (referenceLayerTypeLookup[layer.type]) { + return; + } + handleDataLayerRender(layer, mapState, services, maplibreRef, undefined, timeRange); + }); + }, [timeRange, mapState]); + useEffect(() => { if (!isUpdatingLayerRender && initialLayersLoaded) { return; } @@ -138,7 +138,7 @@ export const LayerControlPanel = memo( setInitialLayersLoaded(true); } setIsUpdatingLayerRender(false); - }, [layers, timeRange]); + }, [layers]); useEffect(() => { const getCurrentVisibleLayers = () => { @@ -210,9 +210,6 @@ export const LayerControlPanel = memo( }; const onClickLayerName = (layer: MapLayerSpecification) => { - if (inDashboardMode) { - return; - } if (hasUnsavedChanges()) { notifications.toasts.addWarning( `You have unsaved changes for ${selectedLayerConfig?.name}` @@ -356,8 +353,13 @@ export const LayerControlPanel = memo( return visibleLayers.includes(layer); }; + if (inDashboardMode) { + return null; + } + + let content; if (isLayerControlVisible) { - return ( + content = ( - + {getReverseLayers().map((layer, index) => { const isLayerSelected = isLayerConfigVisible && @@ -407,7 +405,6 @@ export const LayerControlPanel = memo( index={index} draggableId={layer.id} customDragHandle={true} - isDragDisabled={inDashboardMode} > {(provided) => (
@@ -445,58 +442,54 @@ export const LayerControlPanel = memo( /> - {!inDashboardMode && ( - - - onLayerVisibilityChange(layer)} - aria-label="Hide or show layer" - color="text" - title={ - layerVisibility.get(layer.id) ? 'Hide layer' : 'Show layer' - } - /> - - - onDeleteLayerIconClick(layer)} - aria-label="Delete layer" - color={ - layer.id === selectedLayerConfig?.id ? 'text' : 'danger' - } - title="Delete layer" - disabled={layer.id === selectedLayerConfig?.id} - /> - - - - - - )} + + + onLayerVisibilityChange(layer)} + aria-label="Hide or show layer" + color="text" + title={ + layerVisibility.get(layer.id) ? 'Hide layer' : 'Show layer' + } + /> + + + onDeleteLayerIconClick(layer)} + aria-label="Delete layer" + color={layer.id === selectedLayerConfig?.id ? 'text' : 'danger'} + title="Delete layer" + disabled={layer.id === selectedLayerConfig?.id} + /> + + + + +
@@ -529,26 +522,27 @@ export const LayerControlPanel = memo( setIsNewLayer={setIsNewLayer} mapConfig={mapConfig} layerCount={layers.length} - inDashboardMode={inDashboardMode} /> {deleteLayerModal}
); + } 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.tsx b/public/components/map_container/map_container.tsx index acdd917c..bc95d2e0 100644 --- a/public/components/map_container/map_container.tsx +++ b/public/components/map_container/map_container.tsx @@ -204,22 +204,20 @@ export const MapContainer = ({ zoom: {zoom} -
- {mounted && ( - - )} -
+ {mounted && ( + + )}
); diff --git a/public/components/map_page/map_page.tsx b/public/components/map_page/map_page.tsx index 119dba40..615f0c6f 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,13 +17,17 @@ import { MAP_LAYER_DEFAULT_NAME, OPENSEARCH_MAP_LAYER, } from '../../../common'; -import { MapLayerSpecification } from '../../model/mapLayerType'; +import {MapLayerSpecification, OSMLayerSpecification} from '../../model/mapLayerType'; import { getLayerConfigMap, getInitialMapState } from '../../utils/getIntialConfig'; import { IndexPattern, TimeRange } from '../../../../../src/plugins/data/public'; import { MapState } from '../../model/mapState'; import { ConfigSchema } from '../../../common/config'; -interface Props { +interface MapPageProps { + mapConfig: ConfigSchema; +} + +interface MapComponentProps { mapConfig: ConfigSchema; mapIdFromSavedObject: string; timeRange?: TimeRange; @@ -34,7 +38,7 @@ export const MapComponent = ({ mapConfig, timeRange, inDashboardMode, -}: Props) => { +}: MapComponentProps) => { const { services } = useOpenSearchDashboards(); const { savedObjects: { client: savedObjectsClient }, @@ -66,9 +70,8 @@ export const MapComponent = ({ 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]); } @@ -105,7 +108,7 @@ export const MapComponent = ({ ); }; -export const MapPage = ({ mapConfig }: Props) => { +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..80a49786 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,7 @@ 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 }; - }; - + run: (_anchorElement: any) => { const documentInfo = { title, description, @@ -120,9 +63,20 @@ export const getTopNavConfig = ( const saveModal = ( {}} + originatingApp={originatingApp} + getAppNameFromId={stateTransfer.getAppNameFromId} /> ); showSaveModal(saveModal, I18nContext); @@ -131,3 +85,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 6e0f260e..322bfe5d 100644 --- a/public/components/map_top_nav/top_nav_menu.tsx +++ b/public/components/map_top_nav/top_nav_menu.tsx @@ -4,7 +4,7 @@ */ import React, { useCallback, useEffect, useState } from 'react'; -import { SimpleSavedObject } from 'opensearch-dashboards/public'; +import { SimpleSavedObject } from '../../../../../src/core/public'; import { IndexPattern, Query, TimeRange } from '../../../../../src/plugins/data/public'; import { DASHBOARDS_MAPS_LAYER_TYPE, MAPS_APP_ID } from '../../../common'; import { getTopNavConfig } from './get_top_nav_config'; @@ -26,6 +26,7 @@ interface MapTopNavMenuProps { setMapState: (mapState: MapState) => void; inDashboardMode: boolean; timeRange?: TimeRange; + originatingApp?: string; } export const MapTopNavMenu = ({ @@ -47,6 +48,8 @@ export const MapTopNavMenu = ({ }, chrome, application: { navigateToApp }, + embeddable, + scopedHistory, } = services; const [title, setTitle] = useState(''); @@ -56,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)); @@ -64,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); @@ -77,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); }); }; @@ -104,7 +115,6 @@ export const MapTopNavMenu = ({ setQueryConfig(mapState.query); setIsRefreshPaused(mapState.refreshInterval.pause); setRefreshIntervalValue(mapState.refreshInterval.value); - refreshDataLayerRender(); }, [mapState, timeRange]); const onRefreshChange = useCallback( @@ -126,6 +136,7 @@ export const MapTopNavMenu = ({ setTitle, setDescription, mapState, + originatingApp, })} setMenuMountPoint={setHeaderActionMenu} indexPatterns={layersIndexPatterns || []} diff --git a/public/embeddable/map_component.tsx b/public/embeddable/map_component.tsx index fdef8ab4..306d8a32 100644 --- a/public/embeddable/map_component.tsx +++ b/public/embeddable/map_component.tsx @@ -12,15 +12,15 @@ 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, output }: Props) { - const { savedObjectId } = input; - const [timeRange, setTimeRange] = useState(input.timeRange); +export function MapEmbeddableComponentInner({ embeddable, input }: Props) { + const [timeRange, setTimeRange] = useState(input.timeRange); const services: MapServices = { ...embeddable.getServiceSettings(), }; @@ -33,7 +33,7 @@ export function MapEmbeddableComponentInner({ embeddable, input, output }: Props diff --git a/public/embeddable/map_embeddable.tsx b/public/embeddable/map_embeddable.tsx index 8401df34..51f66e88 100644 --- a/public/embeddable/map_embeddable.tsx +++ b/public/embeddable/map_embeddable.tsx @@ -6,7 +6,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Subscription } from 'rxjs'; -import { MAP_SAVED_OBJECT_TYPE } from '../../common'; +import { MAP_SAVED_OBJECT_TYPE, MAPS_APP_ID } from '../../common'; import { Embeddable, EmbeddableInput, @@ -16,11 +16,11 @@ import { import { MapEmbeddableComponent } from './map_component'; import { ConfigSchema } from '../../common/config'; import { MapSavedObjectAttributes } from '../../common/map_saved_object_attributes'; +import { TimefilterContract } from '../../../../src/plugins/data/public'; export const MAP_EMBEDDABLE = MAP_SAVED_OBJECT_TYPE; export interface MapInput extends EmbeddableInput { - search?: string; savedObjectId: string; } @@ -31,6 +31,8 @@ function getOutput(input: MapInput, editUrl: string, tittle: string): MapOutput editable: true, editUrl, defaultTitle: tittle, + editApp: MAPS_APP_ID, + editPath: input.savedObjectId, }; } @@ -38,8 +40,9 @@ export class MapEmbeddable extends Embeddable { public readonly type = MAP_EMBEDDABLE; private subscription: Subscription; private node?: HTMLElement; - private mapConfig: ConfigSchema; - private services: any; + private readonly mapConfig: ConfigSchema; + private readonly services: any; + private autoRefreshFetchSubscription: Subscription; constructor( initialInput: MapInput, @@ -49,17 +52,22 @@ export class MapEmbeddable extends Embeddable { mapConfig, editUrl, savedMapAttributes, + timeFilter, }: { parent?: IContainer; services: any; mapConfig: ConfigSchema; editUrl: string; savedMapAttributes: MapSavedObjectAttributes; + timeFilter: TimefilterContract; } ) { super(initialInput, getOutput(initialInput, editUrl, savedMapAttributes.title), parent); this.mapConfig = mapConfig; this.services = services; + this.autoRefreshFetchSubscription = timeFilter + .getAutoRefreshFetch$() + .subscribe(this.reload.bind(this)); this.subscription = this.getInput$().subscribe(() => { this.updateOutput(getOutput(this.input, editUrl, savedMapAttributes.title)); }); @@ -73,7 +81,11 @@ export class MapEmbeddable extends Embeddable { ReactDOM.render(, node); } - public reload() {} + public reload() { + if (this.node) { + this.render(this.node); + } + } public destroy() { super.destroy(); @@ -81,6 +93,7 @@ export class MapEmbeddable extends Embeddable { if (this.node) { ReactDOM.unmountComponentAtNode(this.node); } + this.autoRefreshFetchSubscription.unsubscribe(); } public getServiceSettings() { return this.services; diff --git a/public/embeddable/map_embeddable_factory.tsx b/public/embeddable/map_embeddable_factory.tsx index c6a6670a..b1534687 100644 --- a/public/embeddable/map_embeddable_factory.tsx +++ b/public/embeddable/map_embeddable_factory.tsx @@ -7,15 +7,15 @@ import { i18n } from '@osd/i18n'; import { IContainer, EmbeddableFactoryDefinition, - EmbeddableFactory, ErrorEmbeddable, SavedObjectEmbeddableInput, } from '../../../../src/plugins/embeddable/public'; import { MAP_EMBEDDABLE, MapInput, MapOutput, MapEmbeddable } from './map_embeddable'; -import { APP_PATH, MAPS_APP_ICON, MAPS_APP_ID } from '../../common'; +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: { @@ -32,8 +32,6 @@ interface StartServices { mapConfig: ConfigSchema; } -export type MapEmbeddableFactory = EmbeddableFactory; - export class MapEmbeddableFactoryDefinition implements EmbeddableFactoryDefinition { @@ -51,8 +49,8 @@ export class MapEmbeddableFactoryDefinition return true; } + // Maps app will be created from visualization list public canCreateNew() { - // TODO: allow users to create a new map from the dashboard. return false; } @@ -66,6 +64,7 @@ export class MapEmbeddableFactoryDefinition 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( @@ -80,6 +79,7 @@ export class MapEmbeddableFactoryDefinition mapConfig, editUrl: url, savedMapAttributes, + timeFilter, } ); } catch (error) { @@ -88,10 +88,6 @@ export class MapEmbeddableFactoryDefinition }; public async create(initialInput: MapInput, parent?: IContainer) { - const { services } = await this.getStartServices(); - await services.application.navigateToApp(MAPS_APP_ID, { - path: `${APP_PATH.CREATE_MAP}?originatingApp=dashboards`, - }); return undefined; } diff --git a/public/model/layerRenderController.ts b/public/model/layerRenderController.ts index 90c22716..8c24f22d 100644 --- a/public/model/layerRenderController.ts +++ b/public/model/layerRenderController.ts @@ -19,7 +19,7 @@ import { 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 { @@ -29,7 +29,7 @@ interface MaplibreRef { export const prepareDataLayerSource = ( layer: MapLayerSpecification, mapState: MapState, - { data, notifications }: MapServices, + { data, toastNotifications }: MapServices, filters: Filter[] = [], timeRange?: TimeRange ): Promise => { @@ -80,7 +80,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(); } diff --git a/public/plugin.tsx b/public/plugin.tsx index 8d459a15..cb47969c 100644 --- a/public/plugin.tsx +++ b/public/plugin.tsx @@ -17,14 +17,25 @@ import { CustomImportMapPluginSetup, CustomImportMapPluginStart, } from './types'; -import { PLUGIN_NAME, MAPS_APP_ID, MAPS_APP_DISPLAY_NAME } from '../common/constants/shared'; +import { + PLUGIN_NAME, + 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 { MAP_SAVED_OBJECT_TYPE } from '../common'; +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 @@ -55,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(); @@ -72,6 +87,8 @@ export class CustomImportMapPlugin toastNotifications: coreStart.notifications.toasts, history: params.history, data, + embeddable: useEmbeddable, + scopedHistory: params.history, }; params.element.classList.add('mapAppContainer'); // Render the application @@ -81,20 +98,45 @@ export class CustomImportMapPlugin const mapEmbeddableFactory = new MapEmbeddableFactoryDefinition(async () => { const [coreStart, depsStart] = await core.getStartServices(); - const { navigation, data } = depsStart as AppPluginStartDependencies; + const { navigation, data: useData } = depsStart as AppPluginStartDependencies; return { mapConfig, services: { ...coreStart, navigation, + data: useData, toastNotifications: coreStart.notifications.toasts, - data, }, }; }); - 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: [PLUGIN_ID], + toListItem: ({ id, attributes }) => ({ + description: attributes?.description, + editApp: PLUGIN_ID, + editUrl: `${encodeURIComponent(id)}`, + icon: MAPS_APP_ICON, + id, + savedObjectType: MAP_SAVED_OBJECT_TYPE, + title: attributes?.title, + typeTitle: PLUGIN_NAME, + stage: 'production', + }), + }, + }, + }); + const customSetup = async () => { const [coreStart] = await core.getStartServices(); regionMap.addOptionTab({ @@ -124,7 +166,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 c55c48ad..657d6c4c 100644 --- a/public/types.ts +++ b/public/types.ts @@ -8,18 +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 } from '../../../src/plugins/embeddable/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 { @@ -32,8 +33,11 @@ export interface MapServices extends CoreStart { data: DataPublicPluginStart; application: CoreStart['application']; i18n: CoreStart['i18n']; - savedObjects: SavedObjectsClient; + savedObjects: CoreStart['savedObjects']; overlays: CoreStart['overlays']; + embeddable: EmbeddableStart; + scopedHistory: ScopedHistory; + chrome: CoreStart['chrome']; } // eslint-disable-next-line @typescript-eslint/no-empty-interface @@ -42,12 +46,9 @@ 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; }