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..3d071ebb --- /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 { + /** Title of the map */ + title: string; + /** Description of the map */ + description?: string; + /** 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; + /** Version is used to track version differences in saved object mapping */ + version: number; + /** SearchSourceFields is 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 52f2c09b..aab8c3ea 100644 --- a/maps_dashboards/public/application.tsx +++ b/maps_dashboards/public/application.tsx @@ -5,22 +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 }: CoreStart, - { navigation }: AppPluginStartDependencies, - { appBasePath, element }: 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 767188ef..4677f7b6 100644 --- a/maps_dashboards/public/components/app.tsx +++ b/maps_dashboards/public/components/app.tsx @@ -4,34 +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 { MapContainer } from './map_container'; -import { CoreStart } from '../../../../src/core/public'; -import { NavigationPublicPluginStart } from '../../../../src/plugins/navigation/public'; -import { APP_PATH } from '../../common/index'; +import { MapPage } from './map_page'; +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; -} - -export const MapsDashboardsApp = ({ basename, notifications, http }: MapsDashboardsAppDeps) => { +export const MapsDashboardsApp = () => { + const { + services: { appBasePath }, + } = useOpenSearchDashboards(); // Render the application DOM. return ( - +
- } /> - } - /> + } /> + } />
diff --git a/maps_dashboards/public/components/map_page/index.ts b/maps_dashboards/public/components/map_page/index.ts new file mode 100644 index 00000000..a79e0689 --- /dev/null +++ b/maps_dashboards/public/components/map_page/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { MapPage } from './map_page'; diff --git a/maps_dashboards/public/components/map_page/map_page.tsx b/maps_dashboards/public/components/map_page/map_page.tsx new file mode 100644 index 00000000..1a8fc4a7 --- /dev/null +++ b/maps_dashboards/public/components/map_page/map_page.tsx @@ -0,0 +1,17 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { MapContainer } from '../map_container'; +import { MapTopNavMenu } from '../map_top_nav'; + +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 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 bbc4d0fd..11d0170c 100644 --- a/maps_dashboards/public/components/maps_list/maps_list.tsx +++ b/maps_dashboards/public/components/maps_list/maps_list.tsx @@ -7,32 +7,32 @@ 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'; - -export const MapsList = (props: { - notifications: CoreStart['notifications']; - http: CoreStart['http']; -}) => { - const { http } = props; - const find = async (num: number) => { - const res = await http.post('/api/maps-dashboards/example'); - return { - total: num, - hits: res.hits, - }; - }; +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: 'title', + field: 'attributes.title', name: i18n.translate('maps.listing.table.titleColumnName', { defaultMessage: 'Title', }), sortable: true, }, { - field: 'description', + field: 'attributes.description', name: i18n.translate('maps.listing.table.descriptionColumnName', { defaultMessage: 'Description', }), @@ -40,15 +40,38 @@ export const MapsList = (props: { }, ]; - const createItem = 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 findItem = find.bind(null, 3); + const fetchMaps = useCallback(async (): Promise<{ + total: number; + hits: object[]; + }> => { + const res = await savedObjectsClient.find({ + type: 'map', + fields: ['description', 'title'], + }); + return { + total: res.total, + hits: res.savedObjects, + }; + }, [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(() => {}, []); @@ -61,9 +84,9 @@ export const MapsList = (props: { 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 3511e257..df494667 100644 --- a/maps_dashboards/public/types.ts +++ b/maps_dashboards/public/types.ts @@ -3,6 +3,12 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { + AppMountParameters, + CoreStart, + SavedObjectsClient, + ToastsStart, +} from 'opensearch-dashboards/public'; import { NavigationPublicPluginStart } from '../../../src/plugins/navigation/public'; export interface MapsDashboardsPluginSetup { @@ -13,4 +19,13 @@ export interface MapsDashboardsPluginStart {} 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 38a82242..5f5bea6e 100644 --- a/maps_dashboards/server/plugin.ts +++ b/maps_dashboards/server/plugin.ts @@ -10,9 +10,9 @@ import { Plugin, Logger, } from '../../../src/core/server'; - +import { capabilitiesProvider } from './saved_objects/capabilities_provider'; import { MapsDashboardsPluginSetup, MapsDashboardsPluginStart } from './types'; -import { defineRoutes } from './routes'; +import { mapSavedObjectsType } from './saved_objects'; export class MapsDashboardsPlugin implements Plugin { @@ -22,12 +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'); - const router = core.http.createRouter(); - // Register server side APIs - defineRoutes(router); + // Register saved object types + savedObjects.registerType(mapSavedObjectsType); + + // Register capabilities + capabilities.registerProvider(capabilitiesProvider); return {}; } diff --git a/maps_dashboards/server/routes/index.ts b/maps_dashboards/server/routes/index.ts deleted file mode 100644 index e94b016a..00000000 --- a/maps_dashboards/server/routes/index.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -/* - * This is temporary API code for plugin to work, will be - * removed in the future. - */ - -import { IRouter } from '../../../../src/core/server'; - -const SAMPLE_NUMBER = 3; - -export function defineRoutes(router: IRouter) { - router.post( - { - path: '/api/maps-dashboards/example', - validate: false, - }, - async (context, request, response) => { - const hits = []; - for (let i = 0; i < SAMPLE_NUMBER; i++) { - hits.push({ - id: `map ${i}`, - title: `Map ${i}`, - description: `Sample Map ${i} description`, - }); - } - return response.ok({ - body: { - hits, - }, - }); - } - ); -} 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/index.ts b/maps_dashboards/server/saved_objects/index.ts new file mode 100644 index 00000000..416ff0ab --- /dev/null +++ b/maps_dashboards/server/saved_objects/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { mapSavedObjectsType } from './map_saved_object'; diff --git a/maps_dashboards/server/saved_objects/map_saved_object.ts b/maps_dashboards/server/saved_objects/map_saved_object.ts new file mode 100644 index 00000000..aaba48a1 --- /dev/null +++ b/maps_dashboards/server/saved_objects/map_saved_object.ts @@ -0,0 +1,45 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { SavedObjectsType } from 'opensearch-dashboards/server'; + +export const mapSavedObjectsType: SavedObjectsType = { + name: 'map', + hidden: false, + namespaceType: 'agnostic', + management: { + defaultSearchField: 'title', + importableAndExportable: true, + getTitle(obj) { + return obj.attributes.title; + }, + getInAppUrl(obj) { + return { + path: `/app/maps-dashboards#/${encodeURIComponent(obj.id)}`, + uiCapabilitiesPath: 'map.show', + }; + }, + getEditUrl(obj) { + return `/management/opensearch-dashboards/objects/map/${encodeURIComponent(obj.id)}`; + }, + }, + mappings: { + properties: { + title: { 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: {}, +};