diff --git a/maps_dashboards/public/_variables.scss b/maps_dashboards/public/_variables.scss new file mode 100644 index 00000000..98f04c0a --- /dev/null +++ b/maps_dashboards/public/_variables.scss @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +$mapHeaderOffset: 154px; diff --git a/maps_dashboards/public/components/add_layer_panel/add_layer_panel.tsx b/maps_dashboards/public/components/add_layer_panel/add_layer_panel.tsx index 37ed99c1..6e71e362 100644 --- a/maps_dashboards/public/components/add_layer_panel/add_layer_panel.tsx +++ b/maps_dashboards/public/components/add_layer_panel/add_layer_panel.tsx @@ -21,7 +21,7 @@ import { } from '@elastic/eui'; import './add_layer_panel.scss'; import { DOCUMENTS, OPENSEARCH_MAP_LAYER, CUSTOM_MAP, Layer } from '../../../common'; -import { getLayerConfigMap } from '../../utils/getIntialLayerConfig'; +import { getLayerConfigMap } from '../../utils/getIntialConfig'; interface Props { setIsLayerConfigVisible: Function; diff --git a/maps_dashboards/public/components/layer_config/documents_config/document_layer_config_panel.tsx b/maps_dashboards/public/components/layer_config/documents_config/document_layer_config_panel.tsx index 4a41c76f..86854147 100644 --- a/maps_dashboards/public/components/layer_config/documents_config/document_layer_config_panel.tsx +++ b/maps_dashboards/public/components/layer_config/documents_config/document_layer_config_panel.tsx @@ -5,6 +5,7 @@ import React, { Fragment } from 'react'; import { EuiSpacer, EuiTabbedContent } from '@elastic/eui'; +import { IndexPattern } from '../../../../../../src/plugins/data/public'; import { DocumentLayerSpecification } from '../../../model/mapLayerType'; import { LayerBasicSettings } from '../layer_basic_settings'; import { DocumentLayerSource } from './document_layer_source'; @@ -14,6 +15,7 @@ interface Props { selectedLayerConfig: DocumentLayerSpecification; setSelectedLayerConfig: Function; setIsUpdateDisabled: Function; + layersIndexPatterns: IndexPattern[]; isLayerExists: Function; } diff --git a/maps_dashboards/public/components/layer_config/documents_config/document_layer_source.tsx b/maps_dashboards/public/components/layer_config/documents_config/document_layer_source.tsx index f41c8885..1815f2d9 100644 --- a/maps_dashboards/public/components/layer_config/documents_config/document_layer_source.tsx +++ b/maps_dashboards/public/components/layer_config/documents_config/document_layer_source.tsx @@ -20,22 +20,24 @@ import { } from '@elastic/eui'; import { i18n } from '@osd/i18n'; import { FormattedMessage } from '@osd/i18n/react'; +import _, { Dictionary } from 'lodash'; import { Filter, IndexPattern, IndexPatternField } from '../../../../../../src/plugins/data/public'; import { useOpenSearchDashboards } from '../../../../../../src/plugins/opensearch_dashboards_react/public'; import { MapServices } from '../../../types'; import { DocumentLayerSpecification } from '../../../model/mapLayerType'; -import _, { Dictionary } from 'lodash'; interface Props { setSelectedLayerConfig: Function; selectedLayerConfig: DocumentLayerSpecification; setIsUpdateDisabled: Function; + layersIndexPatterns: IndexPattern[]; } export const DocumentLayerSource = ({ setSelectedLayerConfig, selectedLayerConfig, setIsUpdateDisabled, + layersIndexPatterns, }: Props) => { const { services: { @@ -150,10 +152,10 @@ export const DocumentLayerSource = ({ useEffect(() => { const selectIndexPattern = async () => { if (selectedLayerConfig.source.indexPatternId) { - const savedIndexPattern = await indexPatterns.get( - selectedLayerConfig.source.indexPatternId + const selectedIndexPattern = layersIndexPatterns.find( + (ip) => ip.id === selectedLayerConfig.source.indexPatternId ); - setIndexPattern(savedIndexPattern); + setIndexPattern(selectedIndexPattern); } }; selectIndexPattern(); @@ -205,7 +207,7 @@ export const DocumentLayerSource = ({ const shouldTooltipSectionOpen = () => { return ( - selectedLayerConfig.source.showTooltips === true && + selectedLayerConfig.source.showTooltips && selectedLayerConfig.source.tooltipFields?.length > 0 ); }; diff --git a/maps_dashboards/public/components/layer_config/layer_config_panel.tsx b/maps_dashboards/public/components/layer_config/layer_config_panel.tsx index 68933a39..99834193 100644 --- a/maps_dashboards/public/components/layer_config/layer_config_panel.tsx +++ b/maps_dashboards/public/components/layer_config/layer_config_panel.tsx @@ -26,6 +26,7 @@ import { MapLayerSpecification } from '../../model/mapLayerType'; import { BaseMapLayerConfigPanel } from './index'; import { DASHBOARDS_MAPS_LAYER_TYPE } from '../../../common'; import { DocumentLayerConfigPanel } from './documents_config/document_layer_config_panel'; +import { IndexPattern } from '../../../../../src/plugins/data/public'; interface Props { closeLayerConfigPanel: Function; @@ -35,6 +36,8 @@ interface Props { removeLayer: Function; isNewLayer: boolean; setIsNewLayer: Function; + layersIndexPatterns: IndexPattern[]; + updateIndexPatterns: Function; isLayerExists: Function; } @@ -46,6 +49,8 @@ export const LayerConfigPanel = ({ removeLayer, isNewLayer, setIsNewLayer, + layersIndexPatterns, + updateIndexPatterns, isLayerExists, }: Props) => { const [isUpdateDisabled, setIsUpdateDisabled] = useState(false); @@ -107,6 +112,7 @@ export const LayerConfigPanel = ({ selectedLayerConfig={selectedLayerConfig} setSelectedLayerConfig={setSelectedLayerConfig} setIsUpdateDisabled={setIsUpdateDisabled} + layersIndexPatterns={layersIndexPatterns} isLayerExists={isLayerExists} /> )} diff --git a/maps_dashboards/public/components/layer_control_panel/layer_control_panel.tsx b/maps_dashboards/public/components/layer_control_panel/layer_control_panel.tsx index c73963c0..b1cf25d3 100644 --- a/maps_dashboards/public/components/layer_control_panel/layer_control_panel.tsx +++ b/maps_dashboards/public/components/layer_control_panel/layer_control_panel.tsx @@ -21,6 +21,7 @@ import { import { I18nProvider } from '@osd/i18n/react'; import { Map as Maplibre } from 'maplibre-gl'; import './layer_control_panel.scss'; +import { IndexPattern } from '../../../../../src/plugins/data/public'; import { AddLayerPanel } from '../add_layer_panel'; import { LayerConfigPanel } from '../layer_config'; import { MapLayerSpecification } from '../../model/mapLayerType'; @@ -34,12 +35,8 @@ import { import { layersFunctionMap } from '../../model/layersFunctions'; import { useOpenSearchDashboards } from '../../../../../src/plugins/opensearch_dashboards_react/public'; import { MapServices } from '../../types'; -import { - IOpenSearchDashboardsSearchResponse, - isCompleteResponse, - buildOpenSearchQuery, -} from '../../../../../src/plugins/data/common'; -import { IndexPattern } from '../../../../../src/plugins/data/public'; +import { doDataLayerRender } from '../../model/DataLayerController'; +import { MapState } from '../../model/mapState'; interface MaplibreRef { current: Maplibre | null; @@ -49,329 +46,318 @@ interface Props { maplibreRef: MaplibreRef; setLayers: (layers: MapLayerSpecification[]) => void; layers: MapLayerSpecification[]; + layersIndexPatterns: IndexPattern[]; + setLayersIndexPatterns: (indexPatterns: IndexPattern[]) => void; + mapState: MapState; } -const LayerControlPanel = memo(({ maplibreRef, setLayers, layers }: Props) => { - const { - services: { - data: { search, indexPatterns }, - notifications, - }, - } = useOpenSearchDashboards(); +export const LayerControlPanel = memo( + ({ + maplibreRef, + setLayers, + layers, + layersIndexPatterns, + setLayersIndexPatterns, + mapState, + }: Props) => { + const { services } = useOpenSearchDashboards(); + const { + data: { indexPatterns }, + } = services; - const [isLayerConfigVisible, setIsLayerConfigVisible] = useState(false); - const [isLayerControlVisible, setIsLayerControlVisible] = useState(true); - const [selectedLayerConfig, setSelectedLayerConfig] = useState< - MapLayerSpecification | undefined - >(); - const [initialLayersLoaded, setInitialLayersLoaded] = useState(false); - const [addLayerId, setAddLayerId] = useState(''); - const [isUpdatingLayerRender, setIsUpdatingLayerRender] = useState(false); - const [isNewLayer, setIsNewLayer] = useState(false); + const [isLayerConfigVisible, setIsLayerConfigVisible] = useState(false); + const [isLayerControlVisible, setIsLayerControlVisible] = useState(true); + const [selectedLayerConfig, setSelectedLayerConfig] = useState< + MapLayerSpecification | undefined + >(); + const [initialLayersLoaded, setInitialLayersLoaded] = useState(false); + const [addLayerId, setAddLayerId] = useState(''); + const [isUpdatingLayerRender, setIsUpdatingLayerRender] = useState(false); + const [isNewLayer, setIsNewLayer] = useState(false); - useEffect(() => { - if (!isUpdatingLayerRender && initialLayersLoaded) { - return; - } - if (layers.length <= 0) { - return; - } - const doDataLayerRender = async (layer: MapLayerSpecification) => { - if (layer.type === DASHBOARDS_MAPS_LAYER_TYPE.DOCUMENTS) { - const sourceConfig = layer.source; - const indexPatternRefName = sourceConfig?.indexPatternRefName; - const geoField = sourceConfig.geoFieldName; - const sourceFields: string[] = [geoField]; - if (sourceConfig.showTooltips === true && sourceConfig.tooltipFields.length > 0) { - sourceFields.push(...sourceConfig.tooltipFields); + useEffect(() => { + if (!isUpdatingLayerRender && initialLayersLoaded) { + return; + } + if (layers.length <= 0) { + return; + } + if (initialLayersLoaded) { + if (!selectedLayerConfig) { + return; } - let indexPattern: IndexPattern | undefined; - if (layer.source.indexPatternId) { - indexPattern = await indexPatterns.get(layer.source.indexPatternId); + if (selectedLayerConfig.type === DASHBOARDS_MAPS_LAYER_TYPE.OPENSEARCH_MAP) { + layersFunctionMap[selectedLayerConfig.type].render(maplibreRef, selectedLayerConfig); + } else { + updateIndexPatterns(); + doDataLayerRender(selectedLayerConfig, mapState, services, maplibreRef); } - const request = { - params: { - index: indexPatternRefName, - size: layer.source.documentRequestNumber, - body: { - _source: sourceFields, - query: buildOpenSearchQuery(indexPattern, [], layer.source.filters), - }, - }, - }; - const search$ = search.search(request).subscribe({ - next: (response: IOpenSearchDashboardsSearchResponse) => { - if (isCompleteResponse(response)) { - const dataSource = response.rawResponse.hits.hits; - layersFunctionMap[layer.type].render(maplibreRef, layer, dataSource); - layersFunctionMap[layer.type].addTooltip(maplibreRef, layer); - search$.unsubscribe(); - } else { - notifications.toasts.addWarning('An error has occurred when query dataSource'); - search$.unsubscribe(); - } - }, - error: (e: Error) => { - search.showError(e); - }, + if (addLayerId !== selectedLayerConfig.id) { + setSelectedLayerConfig(undefined); + } + } else { + layers.forEach((layer) => { + if (layer.type === DASHBOARDS_MAPS_LAYER_TYPE.OPENSEARCH_MAP) { + layersFunctionMap[layer.type].render(maplibreRef, layer); + } else { + doDataLayerRender(layer, mapState, services, maplibreRef); + } }); + setInitialLayersLoaded(true); } + setIsUpdatingLayerRender(false); + }, [layers]); + + const closeLayerConfigPanel = () => { + setIsLayerConfigVisible(false); + setTimeout(() => { + maplibreRef.current?.resize(); + }, 0); + }; + + const addLayer = (layer: MapLayerSpecification) => { + setLayers([...layers, layer]); + setAddLayerId(layer.id); }; - if (initialLayersLoaded) { + + const updateLayer = () => { if (!selectedLayerConfig) { return; } - if (selectedLayerConfig.type === DASHBOARDS_MAPS_LAYER_TYPE.OPENSEARCH_MAP) { - layersFunctionMap[selectedLayerConfig.type].render(maplibreRef, selectedLayerConfig); + const layersClone = [...layers]; + const index = layersClone.findIndex((layer) => layer.id === selectedLayerConfig.id); + if (index <= -1) { + layersClone.push(selectedLayerConfig); } else { - doDataLayerRender(selectedLayerConfig); - } - if (addLayerId !== selectedLayerConfig.id) { - setSelectedLayerConfig(undefined); + layersClone[index] = { + ...layersClone[index], + ...selectedLayerConfig, + }; } - } else { - layers.forEach((layer) => { - if (layer.type === DASHBOARDS_MAPS_LAYER_TYPE.OPENSEARCH_MAP) { - layersFunctionMap[layer.type].render(maplibreRef, layer); - } else { - doDataLayerRender(layer); - } - }); - setInitialLayersLoaded(true); - } - setIsUpdatingLayerRender(false); - }, [layers]); - - const closeLayerConfigPanel = () => { - setIsLayerConfigVisible(false); - setTimeout(() => { - maplibreRef.current?.resize(); - }, 0); - }; - - const addLayer = (layer: MapLayerSpecification) => { - setLayers([...layers, layer]); - setAddLayerId(layer.id); - }; - - const updateLayer = () => { - if (!selectedLayerConfig) { - return; - } - const layersClone = [...layers]; - const index = layersClone.findIndex((layer) => layer.id === selectedLayerConfig.id); - if (index <= -1) { - layersClone.push(selectedLayerConfig); - } else { - layersClone[index] = { - ...layersClone[index], - ...selectedLayerConfig, - }; - } - setLayers(layersClone); - setIsUpdatingLayerRender(true); - }; - - const removeLayer = (layerId: string) => { - const layersClone = [...layers]; - const index = layersClone.findIndex((layer) => layer.id === layerId); - if (index > -1) { - layersClone.splice(index, 1); setLayers(layersClone); - } - }; + setIsUpdatingLayerRender(true); + }; + + const removeLayer = (layerId: string) => { + const layersClone = [...layers]; + const index = layersClone.findIndex((layer) => layer.id === layerId); + if (index > -1) { + layersClone.splice(index, 1); + setLayers(layersClone); + } + }; - const isLayerExists = (name: string) => { - return layers.findIndex((layer) => layer.name === name) > -1; - }; + const onClickLayerName = (layer: MapLayerSpecification) => { + setSelectedLayerConfig(layer); + setIsLayerConfigVisible(true); + }; + const isLayerExists = (name: string) => { + return layers.findIndex((layer) => layer.name === name) > -1; + }; - const onClickLayerName = (layer: MapLayerSpecification) => { - setSelectedLayerConfig(layer); - setIsLayerConfigVisible(true); - }; + const [layerVisibility, setLayerVisibility] = useState(new Map([])); + layers.forEach((layer) => { + layerVisibility.set(layer.id, layer.visibility === LAYER_VISIBILITY.VISIBLE); + }); - const [layerVisibility, setLayerVisibility] = useState(new Map([])); - layers.forEach((layer) => { - layerVisibility.set(layer.id, layer.visibility === LAYER_VISIBILITY.VISIBLE); - }); + const onDragEnd = ({ source, destination }) => { + if (source && destination) { + const reorderedLayers = euiDragDropReorder(layers, source.index, destination.index); + setLayers(reorderedLayers); + // TODO: Refresh Maplibre layers + } + }; - const onDragEnd = ({ source, destination }) => { - if (source && destination) { - const reorderedLayers = euiDragDropReorder(layers, source.index, destination.index); - setLayers(reorderedLayers); - // TODO: Refresh Maplibre layers - } - }; + const getReverseLayers = () => { + const layersClone = [...layers]; + return layersClone.reverse(); + }; - const getReverseLayers = () => { - const layersClone = [...layers]; - return layersClone.reverse(); - }; + const updateIndexPatterns = async () => { + if (!selectedLayerConfig) { + return; + } + if (selectedLayerConfig.type === DASHBOARDS_MAPS_LAYER_TYPE.OPENSEARCH_MAP) { + return; + } + if (!selectedLayerConfig.source.indexPatternId) { + return; + } + const findIndexPattern = layersIndexPatterns.find( + (indexPattern) => indexPattern.id === selectedLayerConfig.source.indexPatternId + ); + if (!findIndexPattern) { + const newIndexPattern = await indexPatterns.get(selectedLayerConfig.source.indexPatternId); + const cloneLayersIndexPatterns = [...layersIndexPatterns, newIndexPattern]; + setLayersIndexPatterns(cloneLayersIndexPatterns); + } + }; - if (isLayerControlVisible) { - return ( - - - - - - -

Layer

-
-
- - setIsLayerControlVisible((visible) => !visible)} - aria-label="Hide layer control" - color="text" - className="layerControlPanel__visButton" - /> - -
- - - - {getReverseLayers().map((layer, index) => { - const isLayerSelected = - isLayerConfigVisible && - selectedLayerConfig && - selectedLayerConfig.id === layer.id; - return ( - - {(provided) => ( -
- - - onClickLayerName(layer)} - /> - - - - { - if (layer.visibility === LAYER_VISIBILITY.VISIBLE) { - layer.visibility = LAYER_VISIBILITY.NONE; - setLayerVisibility( - new Map(layerVisibility.set(layer.id, false)) - ); - } else { - layer.visibility = LAYER_VISIBILITY.VISIBLE; - setLayerVisibility( - new Map(layerVisibility.set(layer.id, true)) - ); - } - layersFunctionMap[layer.type]?.hide(maplibreRef, layer); - }} - aria-label="Hide or show layer" - color="text" - /> - - - { - layersFunctionMap[layer.type]?.remove(maplibreRef, layer); - removeLayer(layer.id); - }} - aria-label="Delete layer" - color="text" - /> - - - + + + + + +

Layer

+
+
+ + setIsLayerControlVisible((visible) => !visible)} + aria-label="Hide layer control" + color="text" + className="layerControlPanel__visButton" + /> + +
+ + + + {getReverseLayers().map((layer, index) => { + const isLayerSelected = + isLayerConfigVisible && + selectedLayerConfig && + selectedLayerConfig.id === layer.id; + return ( + + {(provided) => ( +
+ + + onClickLayerName(layer)} /> + + + { + if (layer.visibility === LAYER_VISIBILITY.VISIBLE) { + layer.visibility = LAYER_VISIBILITY.NONE; + setLayerVisibility( + new Map(layerVisibility.set(layer.id, false)) + ); + } else { + layer.visibility = LAYER_VISIBILITY.VISIBLE; + setLayerVisibility( + new Map(layerVisibility.set(layer.id, true)) + ); + } + layersFunctionMap[layer.type]?.hide(maplibreRef, layer); + }} + aria-label="Hide or show layer" + color="text" + /> + + + { + layersFunctionMap[layer.type]?.remove(maplibreRef, layer); + removeLayer(layer.id); + }} + aria-label="Delete layer" + color="text" + /> + + + + + - - -
- )} -
- ); - })} -
-
- {isLayerConfigVisible && selectedLayerConfig && ( - +
+ )} +
+ ); + })} +
+
+ {isLayerConfigVisible && selectedLayerConfig && ( + + )} + - )} - -
-
-
+ + + + ); + } + + return ( + + setIsLayerControlVisible((visible) => !visible)} + aria-label="Show layer control" + /> + ); } - - return ( - - setIsLayerControlVisible((visible) => !visible)} - aria-label="Show layer control" - /> - - ); -}); - -export { LayerControlPanel }; +); diff --git a/maps_dashboards/public/components/map_container/map_container.scss b/maps_dashboards/public/components/map_container/map_container.scss index be555172..7453ca94 100644 --- a/maps_dashboards/public/components/map_container/map_container.scss +++ b/maps_dashboards/public/components/map_container/map_container.scss @@ -4,11 +4,12 @@ */ @import "maplibre-gl/dist/maplibre-gl.css"; +@import "../../variables"; /* stylelint-disable no-empty-source */ .map-container { width: 100%; - min-height: calc(100vh - 98px); + min-height: calc(100vh - #{$mapHeaderOffset}); } .maplibregl-ctrl-top-left { @@ -19,8 +20,8 @@ .layerControlPanel-container { z-index: 1; position: absolute; - left: $euiSizeS; - top: $euiSizeS; + margin-left: $euiSizeS; + margin-top: $euiSizeS; } .zoombar { diff --git a/maps_dashboards/public/components/map_container/map_container.tsx b/maps_dashboards/public/components/map_container/map_container.tsx index 8bb88570..22f43a09 100644 --- a/maps_dashboards/public/components/map_container/map_container.tsx +++ b/maps_dashboards/public/components/map_container/map_container.tsx @@ -10,15 +10,26 @@ import { LayerControlPanel } from '../layer_control_panel'; import './map_container.scss'; import { MAP_INITIAL_STATE, MAP_GLYPHS } from '../../../common'; import { MapLayerSpecification } from '../../model/mapLayerType'; +import { IndexPattern } from '../../../../../src/plugins/data/public'; +import { MapState } from '../../model/mapState'; interface MapContainerProps { - mapIdFromUrl: string; setLayers: (layers: MapLayerSpecification[]) => void; layers: MapLayerSpecification[]; + layersIndexPatterns: IndexPattern[]; + setLayersIndexPatterns: (indexPatterns: IndexPattern[]) => void; + maplibreRef: React.MutableRefObject; + mapState: MapState; } -export const MapContainer = ({ mapIdFromUrl, setLayers, layers }: MapContainerProps) => { - const maplibreRef = useRef(null); +export const MapContainer = ({ + setLayers, + layers, + layersIndexPatterns, + setLayersIndexPatterns, + maplibreRef, + mapState, +}: MapContainerProps) => { const mapContainer = useRef(null); const [mounted, setMounted] = useState(false); const [zoom, setZoom] = useState(MAP_INITIAL_STATE.zoom); @@ -56,7 +67,14 @@ export const MapContainer = ({ mapIdFromUrl, setLayers, layers }: MapContainerPr
{mounted && ( - + )}
diff --git a/maps_dashboards/public/components/map_page/map_page.tsx b/maps_dashboards/public/components/map_page/map_page.tsx index 22ae0019..28d073f1 100644 --- a/maps_dashboards/public/components/map_page/map_page.tsx +++ b/maps_dashboards/public/components/map_page/map_page.tsx @@ -3,33 +3,51 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useEffect, useState } from 'react'; +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 { MapContainer } from '../map_container'; import { MapTopNavMenu } from '../map_top_nav'; import { MapLayerSpecification } from '../../model/mapLayerType'; import { MapServices } from '../../types'; import { useOpenSearchDashboards } from '../../../../../src/plugins/opensearch_dashboards_react/public'; import { MapSavedObjectAttributes } from '../../../common/map_saved_object_attributes'; -import { OPENSEARCH_MAP_LAYER } from '../../../common'; -import { getLayerConfigMap } from '../../utils/getIntialLayerConfig'; +import { DASHBOARDS_MAPS_LAYER_TYPE, OPENSEARCH_MAP_LAYER } from '../../../common'; +import { getLayerConfigMap, getInitialMapState } from '../../utils/getIntialConfig'; +import { IndexPattern } from '../../../../../src/plugins/data/public'; +import { MapState } from '../../model/mapState'; export const MapPage = () => { - const [layers, setLayers] = useState([]); - const { id: mapIdFromUrl } = useParams<{ id: string }>(); - const [savedMapObject, setSavedMapObject] = - useState | null>(); 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([]); + const maplibreRef = useRef(null); + const [mapState, setMapState] = useState(getInitialMapState()); useEffect(() => { if (mapIdFromUrl) { savedObjectsClient.get('map', mapIdFromUrl).then((res) => { setSavedMapObject(res); - setLayers(JSON.parse(res.attributes.layerList as string)); + const layerList: MapLayerSpecification[] = JSON.parse(res.attributes.layerList as string); + const savedMapState: MapState = JSON.parse(res.attributes.mapState as string); + 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); }); } else { const initialDefaultLayer: MapLayerSpecification = @@ -40,8 +58,23 @@ export const MapPage = () => { return (
- - + +
); }; diff --git a/maps_dashboards/public/components/map_top_nav/get_top_nav_config.tsx b/maps_dashboards/public/components/map_top_nav/get_top_nav_config.tsx index 3b7ef206..4269c356 100644 --- a/maps_dashboards/public/components/map_top_nav/get_top_nav_config.tsx +++ b/maps_dashboards/public/components/map_top_nav/get_top_nav_config.tsx @@ -13,6 +13,7 @@ import { checkForDuplicateTitle, } from '../../../../../src/plugins/saved_objects/public'; import { MapServices } from '../../types'; +import { MapState } from '../../model/mapState'; const SAVED_OBJECT_TYPE = 'map'; @@ -23,6 +24,7 @@ interface GetTopNavConfigParams { description: string; setTitle: (title: string) => void; setDescription: (description: string) => void; + mapState: MapState; } export const getTopNavConfig = ( @@ -33,7 +35,15 @@ export const getTopNavConfig = ( history, overlays, }: MapServices, - { mapIdFromUrl, layers, title, description, setTitle, setDescription }: GetTopNavConfigParams + { + mapIdFromUrl, + layers, + title, + description, + setTitle, + setDescription, + mapState, + }: GetTopNavConfigParams ) => { const topNavConfig: TopNavMenuData[] = [ { @@ -50,6 +60,7 @@ export const getTopNavConfig = ( title: newTitle, description: newDescription, layerList: JSON.stringify(layers), + mapState: JSON.stringify(mapState), }; try { await checkForDuplicateTitle( diff --git a/maps_dashboards/public/components/map_top_nav/top_nav_menu.tsx b/maps_dashboards/public/components/map_top_nav/top_nav_menu.tsx index 00d726d6..74a4d043 100644 --- a/maps_dashboards/public/components/map_top_nav/top_nav_menu.tsx +++ b/maps_dashboards/public/components/map_top_nav/top_nav_menu.tsx @@ -5,20 +5,36 @@ import React, { useCallback, useEffect, useState } from 'react'; import { SimpleSavedObject } from 'opensearch-dashboards/public'; -import { PLUGIN_ID } from '../../../common'; +import { IndexPattern, Query, TimeRange } from '../../../../../src/plugins/data/public'; +import { DASHBOARDS_MAPS_LAYER_TYPE, PLUGIN_ID } from '../../../common'; import { getTopNavConfig } from './get_top_nav_config'; import { useOpenSearchDashboards } from '../../../../../src/plugins/opensearch_dashboards_react/public'; import { MapServices } from '../../types'; import { MapSavedObjectAttributes } from '../../../common/map_saved_object_attributes'; import { getSavedMapBreadcrumbs } from '../../utils/breadcrumbs'; +import { doDataLayerRender } from '../../model/DataLayerController'; +import { MapLayerSpecification } from '../../model/mapLayerType'; +import { MapState } from '../../model/mapState'; interface MapTopNavMenuProps { mapIdFromUrl: string; - layers: any; + layers: MapLayerSpecification[]; savedMapObject: SimpleSavedObject | null | undefined; + layersIndexPatterns: IndexPattern[]; + maplibreRef: any; + mapState: MapState; + setMapState: (mapState: MapState) => void; } -export const MapTopNavMenu = ({ mapIdFromUrl, savedMapObject, layers }: MapTopNavMenuProps) => { +export const MapTopNavMenu = ({ + mapIdFromUrl, + savedMapObject, + layers, + layersIndexPatterns, + maplibreRef, + mapState, + setMapState, +}: MapTopNavMenuProps) => { const { services } = useOpenSearchDashboards(); const { setHeaderActionMenu, @@ -31,7 +47,11 @@ export const MapTopNavMenu = ({ mapIdFromUrl, savedMapObject, layers }: MapTopNa const [title, setTitle] = useState(''); const [description, setDescription] = useState(''); - + const [dateFrom, setDateFrom] = useState(''); + const [dateTo, setDateTo] = useState(''); + const [queryConfig, setQueryConfig] = useState({ query: '', language: 'kuery' }); + const [refreshIntervalValue, setRefreshIntervalValue] = useState(60000); + const [isRefreshPaused, setIsRefreshPaused] = useState(false); const changeTitle = useCallback( (newTitle: string) => { chrome.setBreadcrumbs(getSavedMapBreadcrumbs(newTitle, navigateToApp)); @@ -51,6 +71,33 @@ export const MapTopNavMenu = ({ mapIdFromUrl, savedMapObject, layers }: MapTopNa changeTitle(title || 'Create'); }, [title, changeTitle]); + const refreshDataLayerRender = useCallback(() => { + layers.forEach((layer: MapLayerSpecification) => { + if (layer.type === DASHBOARDS_MAPS_LAYER_TYPE.OPENSEARCH_MAP) { + return; + } + doDataLayerRender(layer, mapState, services, maplibreRef); + }); + }, [layers, mapState, maplibreRef, services]); + + const handleQuerySubmit = ({ query, dateRange }: { query?: Query; dateRange: TimeRange }) => { + if (query) { + setMapState({ ...mapState, query }); + } + if (dateRange) { + setMapState({ ...mapState, timeRange: dateRange }); + } + }; + + useEffect(() => { + setDateFrom(mapState.timeRange.from); + setDateTo(mapState.timeRange.to); + setQueryConfig(mapState.query); + setIsRefreshPaused(mapState.refreshInterval.pause); + setRefreshIntervalValue(mapState.refreshInterval.value); + refreshDataLayerRender(); + }, [mapState, refreshDataLayerRender]); + return ( ); }; diff --git a/maps_dashboards/public/model/DataLayerController.ts b/maps_dashboards/public/model/DataLayerController.ts new file mode 100644 index 00000000..34aeec84 --- /dev/null +++ b/maps_dashboards/public/model/DataLayerController.ts @@ -0,0 +1,78 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Map as Maplibre } from 'maplibre-gl'; +import { MapLayerSpecification } from './mapLayerType'; +import { DASHBOARDS_MAPS_LAYER_TYPE } from '../../common'; +import { + buildOpenSearchQuery, + getTime, + IOpenSearchDashboardsSearchResponse, + isCompleteResponse, +} from '../../../../src/plugins/data/common'; +import { layersFunctionMap } from './layersFunctions'; +import { MapServices } from '../types'; +import { MapState } from './mapState'; + +interface MaplibreRef { + current: Maplibre | null; +} + +export const doDataLayerRender = async ( + layer: MapLayerSpecification, + mapState: MapState, + { data, notifications }: MapServices, + maplibreRef: MaplibreRef +) => { + if (layer.type === DASHBOARDS_MAPS_LAYER_TYPE.DOCUMENTS) { + const sourceConfig = layer.source; + const indexPattern = await data.indexPatterns.get(sourceConfig.indexPatternId); + const indexPatternRefName = sourceConfig?.indexPatternRefName; + const geoField = sourceConfig.geoFieldName; + const sourceFields: string[] = [geoField]; + if (sourceConfig.showTooltips && sourceConfig.tooltipFields.length > 0) { + sourceFields.push(...sourceConfig.tooltipFields); + } + let buildQuery; + if (indexPattern) { + const timeFilters = getTime(indexPattern, mapState.timeRange); + buildQuery = buildOpenSearchQuery( + indexPattern, + [], + [ + ...(layer.source.filters ? layer.source.filters : []), + ...(timeFilters ? [timeFilters] : []), + ] + ); + } + const request = { + params: { + index: indexPatternRefName, + size: layer.source.documentRequestNumber, + body: { + _source: sourceFields, + query: buildQuery, + }, + }, + }; + + const search$ = data.search.search(request).subscribe({ + next: (response: IOpenSearchDashboardsSearchResponse) => { + if (isCompleteResponse(response)) { + const dataSource = response.rawResponse.hits.hits; + layersFunctionMap[layer.type].render(maplibreRef, layer, dataSource); + layersFunctionMap[layer.type].addTooltip(maplibreRef, layer); + search$.unsubscribe(); + } else { + notifications.toasts.addWarning('An error has occurred when query dataSource'); + search$.unsubscribe(); + } + }, + error: (e: Error) => { + data.search.showError(e); + }, + }); + } +}; diff --git a/maps_dashboards/public/model/mapLayerType.ts b/maps_dashboards/public/model/mapLayerType.ts index 54c9eb0c..ec0e4de1 100644 --- a/maps_dashboards/public/model/mapLayerType.ts +++ b/maps_dashboards/public/model/mapLayerType.ts @@ -34,7 +34,7 @@ export type DocumentLayerSpecification = { visibility: string; source: { indexPatternRefName: string; - indexPatternId?: string; + indexPatternId: string; geoFieldType: 'geo_point' | 'geo_shape'; geoFieldName: string; documentRequestNumber: number; diff --git a/maps_dashboards/public/model/mapState.ts b/maps_dashboards/public/model/mapState.ts new file mode 100644 index 00000000..4d4ceddc --- /dev/null +++ b/maps_dashboards/public/model/mapState.ts @@ -0,0 +1,10 @@ +import { Query, TimeRange } from '../../../../src/plugins/data/common'; + +export interface MapState { + timeRange: TimeRange; + query: Query; + refreshInterval: { + pause: boolean; + value: number; + }; +} diff --git a/maps_dashboards/public/utils/getIntialLayerConfig.ts b/maps_dashboards/public/utils/getIntialConfig.ts similarity index 88% rename from maps_dashboards/public/utils/getIntialLayerConfig.ts rename to maps_dashboards/public/utils/getIntialConfig.ts index cf070848..56d9bbbd 100644 --- a/maps_dashboards/public/utils/getIntialLayerConfig.ts +++ b/maps_dashboards/public/utils/getIntialConfig.ts @@ -21,6 +21,7 @@ import { OPENSEARCH_MAP_LAYER, CUSTOM_MAP, } from '../../common'; +import { MapState } from '../model/mapState'; export const getLayerConfigMap = () => ({ [OPENSEARCH_MAP_LAYER.type]: { @@ -97,3 +98,23 @@ export const getStyleColor = () => { borderColor: initialColor, }; }; + +export const getInitialMapState = (): MapState => { + const timeRange = { + from: 'now-15m', + to: 'now', + }; + const query = { + query: '', + language: 'kuery', + }; + const refreshInterval = { + pause: true, + value: 12000, + }; + return { + timeRange, + query, + refreshInterval, + }; +};