diff --git a/maps_dashboards/common/index.ts b/maps_dashboards/common/index.ts index 166f2977..a45056be 100644 --- a/maps_dashboards/common/index.ts +++ b/maps_dashboards/common/index.ts @@ -29,16 +29,3 @@ export const LAYER_VISIBILITY = { NONE: 'none', VISIBLE: 'visible', }; - -export interface MapSavedObjectAttributes { - /** Tile of the map */ - title: string; - /** Description of the map */ - description?: string; - /** Map state of the map, which could include current zoom level, lat, lng etc. */ - mapState?: string; - /** Maps-dashboards layers of the map */ - layerList?: string; - /** UI state of the map */ - uiState?: string; -} diff --git a/maps_dashboards/common/map_saved_object_attributes.ts b/maps_dashboards/common/map_saved_object_attributes.ts new file mode 100644 index 00000000..16cd8087 --- /dev/null +++ b/maps_dashboards/common/map_saved_object_attributes.ts @@ -0,0 +1,25 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { SavedObjectAttributes } from 'opensearch-dashboards/server'; + +export interface MapSavedObjectAttributes extends SavedObjectAttributes { + /** Tile of the map */ + title: string; + /** Description of the map */ + description?: string; + /** Map state of the map, which could include current zoom level, lat, lng etc. */ + mapState?: string; + /** Maps-dashboards layers of the map */ + layerList?: string; + /** UI state of the map */ + uiState?: string; + /** Used to track version differences in saved object mapping */ + version: number; + /** Used to reference other saved objects */ + searchSourceFields?: { + index?: string; + }; +} diff --git a/maps_dashboards/opensearch_dashboards.json b/maps_dashboards/opensearch_dashboards.json index de5131c3..0920bf42 100644 --- a/maps_dashboards/opensearch_dashboards.json +++ b/maps_dashboards/opensearch_dashboards.json @@ -4,6 +4,6 @@ "opensearchDashboardsVersion": "3.0.0", "server": true, "ui": true, - "requiredPlugins": ["navigation", "opensearchDashboardsReact"], + "requiredPlugins": ["navigation", "opensearchDashboardsReact", "savedObjects"], "optionalPlugins": [] } diff --git a/maps_dashboards/public/application.tsx b/maps_dashboards/public/application.tsx index 44aa748b..aab8c3ea 100644 --- a/maps_dashboards/public/application.tsx +++ b/maps_dashboards/public/application.tsx @@ -5,24 +5,16 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import { AppMountParameters, CoreStart } from '../../../src/core/public'; -import { AppPluginStartDependencies } from './types'; +import { AppMountParameters } from '../../../src/core/public'; +import { MapServices } from './types'; import { MapsDashboardsApp } from './components/app'; +import { OpenSearchDashboardsContextProvider } from '../../../src/plugins/opensearch_dashboards_react/public'; -export const renderApp = ( - { notifications, http, savedObjects }: CoreStart, - { navigation }: AppPluginStartDependencies, - { appBasePath, element, setHeaderActionMenu }: AppMountParameters -) => { +export const renderApp = ({ element }: AppMountParameters, services: MapServices) => { ReactDOM.render( - , + + + , element ); diff --git a/maps_dashboards/public/components/app.tsx b/maps_dashboards/public/components/app.tsx index 8561da90..4677f7b6 100644 --- a/maps_dashboards/public/components/app.tsx +++ b/maps_dashboards/public/components/app.tsx @@ -4,58 +4,26 @@ */ import React from 'react'; -import { HashRouter as Router, Route, Switch } from 'react-router-dom'; +import { Router, Route, Switch } from 'react-router-dom'; import { I18nProvider } from '@osd/i18n/react'; import { MapsList } from './maps_list'; import { MapPage } from './map_page'; -import { AppMountParameters, CoreStart } from '../../../../src/core/public'; -import { NavigationPublicPluginStart } from '../../../../src/plugins/navigation/public'; -import { APP_PATH } from '../../common/index'; +import { APP_PATH } from '../../common'; +import { useOpenSearchDashboards } from '../../../../src/plugins/opensearch_dashboards_react/public'; +import { MapServices } from '../types'; -interface MapsDashboardsAppDeps { - basename: string; - notifications: CoreStart['notifications']; - http: CoreStart['http']; - navigation: NavigationPublicPluginStart; - savedObjectsClient: CoreStart['savedObjects']['client']; - setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; -} - -export const MapsDashboardsApp = ({ - basename, - notifications, - http, - savedObjectsClient, - navigation, - setHeaderActionMenu, -}: MapsDashboardsAppDeps) => { +export const MapsDashboardsApp = () => { + const { + services: { appBasePath }, + } = useOpenSearchDashboards(); // Render the application DOM. return ( - +
- ( - - )} - /> - ( - - )} - /> + } /> + } />
diff --git a/maps_dashboards/public/components/map_page/map_page.tsx b/maps_dashboards/public/components/map_page/map_page.tsx index 37eaef1a..1a8fc4a7 100644 --- a/maps_dashboards/public/components/map_page/map_page.tsx +++ b/maps_dashboards/public/components/map_page/map_page.tsx @@ -4,52 +4,13 @@ */ import React from 'react'; -import { i18n } from '@osd/i18n'; -import { AppMountParameters, CoreStart } from 'opensearch-dashboards/public'; -import { - NavigationPublicPluginStart as NavigationStart, - TopNavMenuData, -} from '../../../../../src/plugins/navigation/public'; -import { PLUGIN_ID } from '../../../common'; import { MapContainer } from '../map_container'; +import { MapTopNavMenu } from '../map_top_nav'; -interface MapContainerProps { - navigation: NavigationStart; - setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; - savedObjectsClient: CoreStart['savedObjects']['client']; -} - -export const MapPage = ({ - navigation, - setHeaderActionMenu, - savedObjectsClient, -}: MapContainerProps) => { - const topNavMenuConfigs: TopNavMenuData[] = []; - topNavMenuConfigs.push({ - label: i18n.translate('maps.topNav.saveMapButtonLabel', { - defaultMessage: `Save`, - }), - run: async () => { - await savedObjectsClient.create('map', { - title: 'test to add', - description: 'test to add the description', - }); - }, - iconType: 'save', - }); - const renderTopNavMenu = () => { - const { TopNavMenu } = navigation.ui; - return ( - - ); - }; +export const MapPage = () => { return (
- {renderTopNavMenu()} +
); 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 new file mode 100644 index 00000000..710115bb --- /dev/null +++ b/maps_dashboards/public/components/map_top_nav/get_top_nav_config.tsx @@ -0,0 +1,72 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { i18n } from '@osd/i18n'; +import { TopNavMenuData } from '../../../../../src/plugins/navigation/public'; +import { + OnSaveProps, + SavedObjectSaveModalOrigin, + showSaveModal, +} from '../../../../../src/plugins/saved_objects/public'; +import { useOpenSearchDashboards } from '../../../../../src/plugins/opensearch_dashboards_react/public'; +import { MapServices } from '../../types'; + +export const getTopNavConfig = () => { + const { + services: { + notifications: { toasts }, + i18n: { Context: I18nContext }, + savedObjects: { client: savedObjectsClient }, + }, + // eslint-disable-next-line react-hooks/rules-of-hooks + } = useOpenSearchDashboards(); + const topNavConfig: TopNavMenuData[] = [ + { + iconType: 'save', + emphasize: true, + id: 'save', + label: i18n.translate('maps.topNav.saveMapButtonLabel', { + defaultMessage: `Save`, + }), + run: (_anchorElement) => { + const onModalSave = async (onSaveProps: OnSaveProps) => { + const savedMap = await savedObjectsClient.create('map', { + title: onSaveProps.newTitle, + description: onSaveProps.newDescription, + // TODO: Integrate other attributes to saved object + }); + const id = savedMap.id; + if (id) { + toasts.addSuccess({ + title: i18n.translate('map.topNavMenu.saveMap.successNotificationText', { + defaultMessage: `Saved ${onSaveProps.newTitle}`, + values: { + visTitle: savedMap.attributes.title, + }, + }), + }); + } + return { id }; + }; + + const documentInfo = { + title: '', + description: '', + }; + const saveModal = ( + {}} + /> + ); + showSaveModal(saveModal, I18nContext); + }, + }, + ]; + return topNavConfig; +}; diff --git a/maps_dashboards/public/components/map_top_nav/index.ts b/maps_dashboards/public/components/map_top_nav/index.ts new file mode 100644 index 00000000..c732aa00 --- /dev/null +++ b/maps_dashboards/public/components/map_top_nav/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { MapTopNavMenu } from './top_nav_menu'; 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 new file mode 100644 index 00000000..3b9b11c4 --- /dev/null +++ b/maps_dashboards/public/components/map_top_nav/top_nav_menu.tsx @@ -0,0 +1,28 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { PLUGIN_ID } from '../../../common'; +import { getTopNavConfig } from './get_top_nav_config'; +import { useOpenSearchDashboards } from '../../../../../src/plugins/opensearch_dashboards_react/public'; +import { MapServices } from '../../types'; + +export const MapTopNavMenu = () => { + const { + services: { + setHeaderActionMenu, + navigation: { + ui: { TopNavMenu }, + }, + }, + } = useOpenSearchDashboards(); + return ( + + ); +}; diff --git a/maps_dashboards/public/components/maps_list/maps_list.tsx b/maps_dashboards/public/components/maps_list/maps_list.tsx index 41962af2..11d0170c 100644 --- a/maps_dashboards/public/components/maps_list/maps_list.tsx +++ b/maps_dashboards/public/components/maps_list/maps_list.tsx @@ -7,17 +7,22 @@ import { i18n } from '@osd/i18n'; import React, { useCallback } from 'react'; import { I18nProvider } from '@osd/i18n/react'; import { EuiPage, EuiPageBody, EuiPageContentBody } from '@elastic/eui'; -import { CoreStart } from 'opensearch-dashboards/public'; -import { TableListView } from '../../../../../src/plugins/opensearch_dashboards_react/public'; -import { MapSavedObjectAttributes } from '../../../common'; - -export const MapsList = (props: { - notifications: CoreStart['notifications']; - http: CoreStart['http']; - savedObjectsClient: CoreStart['savedObjects']['client']; -}) => { - const { http, savedObjectsClient } = props; +import { + TableListView, + useOpenSearchDashboards, +} from '../../../../../src/plugins/opensearch_dashboards_react/public'; +import { MapSavedObjectAttributes } from '../../../common/map_saved_object_attributes'; +import { MapServices } from '../../types'; +export const MapsList = () => { + const { + services: { + notifications: { toasts }, + http: { basePath }, + savedObjects: { client: savedObjectsClient }, + application: { navigateToUrl }, + }, + } = useOpenSearchDashboards(); const tableColumns = [ { field: 'attributes.title', @@ -35,9 +40,9 @@ export const MapsList = (props: { }, ]; - const createMap = useCallback(() => { - window.location.href = http.basePath.prepend('/app/maps-dashboards/#/create-map'); - }, [http.basePath]); + const createMap = () => { + navigateToUrl(basePath.prepend('/app/maps-dashboards/create-map')); + }; const fetchMaps = useCallback(async (): Promise<{ total: number; @@ -53,9 +58,20 @@ export const MapsList = (props: { }; }, [savedObjectsClient]); - const deleteItems = async () => { - await Promise.all([]); - }; + const deleteMaps = useCallback( + async (selectedItems: object[]) => { + await Promise.all( + selectedItems.map((item: any) => savedObjectsClient.delete(item.type, item.id)) + ).catch((error) => { + toasts.addError(error, { + title: i18n.translate('map.mapListingDeleteErrorTitle', { + defaultMessage: 'Error deleting map', + }), + }); + }); + }, + [savedObjectsClient, toasts] + ); const editItem = useCallback(() => {}, []); @@ -70,7 +86,7 @@ export const MapsList = (props: { headingId="mapsListingHeading" createItem={createMap} findItems={fetchMaps} - deleteItems={deleteItems} + deleteItems={deleteMaps} editItem={editItem} tableColumns={tableColumns} listingLimit={10} @@ -86,7 +102,7 @@ export const MapsList = (props: { tableListTitle={i18n.translate('maps.listing.table.listTitle', { defaultMessage: 'Maps', })} - toastNotifications={props.notifications.toasts} + toastNotifications={toasts} /> diff --git a/maps_dashboards/public/plugin.ts b/maps_dashboards/public/plugin.ts index ade9f04b..334452d0 100644 --- a/maps_dashboards/public/plugin.ts +++ b/maps_dashboards/public/plugin.ts @@ -15,6 +15,7 @@ import { MapsDashboardsPluginSetup, MapsDashboardsPluginStart, AppPluginStartDependencies, + MapServices, } from './types'; import { PLUGIN_NAME, PLUGIN_ID } from '../common'; @@ -31,8 +32,17 @@ export class MapsDashboardsPlugin const { renderApp } = await import('./application'); // Get start services as specified in opensearch_dashboards.json const [coreStart, depsStart] = await core.getStartServices(); + const { navigation } = depsStart as AppPluginStartDependencies; + const services: MapServices = { + ...coreStart, + setHeaderActionMenu: params.setHeaderActionMenu, + appBasePath: params.history, + element: params.element, + navigation, + toastNotifications: coreStart.notifications.toasts, + }; // Render the application - return renderApp(coreStart, depsStart as AppPluginStartDependencies, params); + return renderApp(params, services); }, }); diff --git a/maps_dashboards/public/types.ts b/maps_dashboards/public/types.ts index 505579d7..df494667 100644 --- a/maps_dashboards/public/types.ts +++ b/maps_dashboards/public/types.ts @@ -3,7 +3,12 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { SavedObjectsClient } from 'opensearch-dashboards/public'; +import { + AppMountParameters, + CoreStart, + SavedObjectsClient, + ToastsStart, +} from 'opensearch-dashboards/public'; import { NavigationPublicPluginStart } from '../../../src/plugins/navigation/public'; export interface MapsDashboardsPluginSetup { @@ -16,3 +21,11 @@ export interface AppPluginStartDependencies { navigation: NavigationPublicPluginStart; savedObjects: SavedObjectsClient; } + +export interface MapServices extends CoreStart { + setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; + appBasePath: AppMountParameters['history']; + element: AppMountParameters['element']; + navigation: NavigationPublicPluginStart; + toastNotifications: ToastsStart; +} diff --git a/maps_dashboards/server/plugin.ts b/maps_dashboards/server/plugin.ts index 2cb00b4c..5f5bea6e 100644 --- a/maps_dashboards/server/plugin.ts +++ b/maps_dashboards/server/plugin.ts @@ -10,7 +10,7 @@ import { Plugin, Logger, } from '../../../src/core/server'; - +import { capabilitiesProvider } from './saved_objects/capabilities_provider'; import { MapsDashboardsPluginSetup, MapsDashboardsPluginStart } from './types'; import { mapSavedObjectsType } from './saved_objects'; @@ -22,9 +22,14 @@ export class MapsDashboardsPlugin this.logger = initializerContext.logger.get(); } - public setup(core: CoreSetup) { + public setup({ capabilities, http, savedObjects }: CoreSetup) { this.logger.debug('mapsDashboards: Setup'); - core.savedObjects.registerType(mapSavedObjectsType); + + // Register saved object types + savedObjects.registerType(mapSavedObjectsType); + + // Register capabilities + capabilities.registerProvider(capabilitiesProvider); return {}; } diff --git a/maps_dashboards/server/saved_objects/capabilities_provider.ts b/maps_dashboards/server/saved_objects/capabilities_provider.ts new file mode 100644 index 00000000..dde86a4f --- /dev/null +++ b/maps_dashboards/server/saved_objects/capabilities_provider.ts @@ -0,0 +1,17 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export const capabilitiesProvider = () => ({ + map: { + // TODO: investigate which capabilities we need to provide + // createNew: true, + // createShortUrl: true, + // delete: true, + show: true, + // showWriteControls: true, + // save: true, + // saveQuery: true, + }, +}); diff --git a/maps_dashboards/server/saved_objects/map_saved_object.ts b/maps_dashboards/server/saved_objects/map_saved_object.ts index 064fdf6b..aaba48a1 100644 --- a/maps_dashboards/server/saved_objects/map_saved_object.ts +++ b/maps_dashboards/server/saved_objects/map_saved_object.ts @@ -28,10 +28,17 @@ export const mapSavedObjectsType: SavedObjectsType = { mappings: { properties: { title: { type: 'text' }, - description: { type: 'text' }, - layerList: { type: 'text' }, - uiState: { type: 'text' }, - mapState: { type: 'text' }, + description: { type: 'text', index: false }, + layerList: { type: 'text', index: false }, + uiState: { type: 'text', index: false }, + mapState: { type: 'text', index: false }, + version: { type: 'integer' }, + // Need to add a kibanaSavedObjectMeta attribute here to follow the current saved object flow + // When we save a saved object, the saved object plugin will extract the search source into two parts + // Some information will be put into kibanaSavedObjectMeta while others will be created as a reference object and pushed to the reference array + kibanaSavedObjectMeta: { + properties: { searchSourceJSON: { type: 'text', index: false } }, + }, }, }, migrations: {},