/* * SPDX-License-Identifier: Apache-2.0 * * The OpenSearch Contributors require contributions made to * this file be licensed under the Apache-2.0 license or a * compatible open source license. * * Any modifications Copyright OpenSearch Contributors. See * GitHub history for details. */ /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright * ownership. Elasticsearch B.V. licenses this file to you under * the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import * as React from 'react'; import { BehaviorSubject } from 'rxjs'; import { filter, map } from 'rxjs/operators'; import { i18n } from '@osd/i18n'; import { FormattedMessage } from '@osd/i18n/react'; import { App, AppMountParameters, AppUpdater, CoreSetup, CoreStart, Plugin, PluginInitializerContext, SavedObjectsClientContract, ScopedHistory, } from 'src/core/public'; import { UrlForwardingSetup, UrlForwardingStart } from 'src/plugins/url_forwarding/public'; import { isEmpty } from 'lodash'; import { createHashHistory } from 'history'; import { UsageCollectionSetup } from '../../usage_collection/public'; import { CONTEXT_MENU_TRIGGER, EmbeddableSetup, EmbeddableStart, SavedObjectEmbeddableInput, EmbeddableInput, PANEL_NOTIFICATION_TRIGGER, } from '../../embeddable/public'; import { DataPublicPluginSetup, DataPublicPluginStart, opensearchFilters } from '../../data/public'; import { SharePluginSetup, SharePluginStart, UrlGeneratorContract } from '../../share/public'; import { UiActionsSetup, UiActionsStart } from '../../ui_actions/public'; import { Start as InspectorStartContract } from '../../inspector/public'; import { NavigationPublicPluginStart as NavigationStart } from '../../navigation/public'; import { getSavedObjectFinder, SavedObjectLoader, SavedObjectsStart, showSaveModal, } from '../../saved_objects/public'; import { ExitFullScreenButton as ExitFullScreenButtonUi, ExitFullScreenButtonProps, } from '../../opensearch_dashboards_react/public'; import { createOsdUrlTracker, Storage, createOsdUrlStateStorage, withNotifyOnErrors, } from '../../opensearch_dashboards_utils/public'; import { initAngularBootstrap, OpenSearchDashboardsLegacySetup, OpenSearchDashboardsLegacyStart, } from '../../opensearch_dashboards_legacy/public'; import { FeatureCatalogueCategory, HomePublicPluginSetup } from '../../../plugins/home/public'; import { DEFAULT_APP_CATEGORIES } from '../../../core/public'; import { ACTION_CLONE_PANEL, ACTION_EXPAND_PANEL, ACTION_REPLACE_PANEL, ClonePanelAction, ClonePanelActionContext, createDashboardContainerByValueRenderer, DASHBOARD_CONTAINER_TYPE, DashboardContainerFactory, DashboardContainerFactoryDefinition, ExpandPanelAction, ExpandPanelActionContext, ReplacePanelAction, ReplacePanelActionContext, ACTION_UNLINK_FROM_LIBRARY, UnlinkFromLibraryActionContext, UnlinkFromLibraryAction, ACTION_ADD_TO_LIBRARY, AddToLibraryActionContext, AddToLibraryAction, ACTION_LIBRARY_NOTIFICATION, LibraryNotificationActionContext, LibraryNotificationAction, } from './application'; import { createDashboardUrlGenerator, DASHBOARD_APP_URL_GENERATOR, DashboardUrlGeneratorState, } from './url_generator'; import { createSavedDashboardLoader } from './saved_dashboards'; import { DashboardConstants } from './dashboard_constants'; import { addEmbeddableToDashboardUrl } from './url_utils/url_helper'; import { PlaceholderEmbeddableFactory } from './application/embeddable/placeholder'; import { UrlGeneratorState } from '../../share/public'; import { AttributeService } from '.'; import { AttributeServiceOptions, ATTRIBUTE_SERVICE_KEY, } from './attribute_service/attribute_service'; import { DashboardProvider, DashboardServices } from './types'; declare module '../../share/public' { export interface UrlGeneratorStateMapping { [DASHBOARD_APP_URL_GENERATOR]: UrlGeneratorState; } } export type DashboardUrlGenerator = UrlGeneratorContract; export interface DashboardFeatureFlagConfig { allowByValueEmbeddables: boolean; } interface SetupDependencies { data: DataPublicPluginSetup; embeddable: EmbeddableSetup; home?: HomePublicPluginSetup; opensearchDashboardsLegacy: OpenSearchDashboardsLegacySetup; urlForwarding: UrlForwardingSetup; share?: SharePluginSetup; uiActions: UiActionsSetup; usageCollection?: UsageCollectionSetup; } interface StartDependencies { data: DataPublicPluginStart; opensearchDashboardsLegacy: OpenSearchDashboardsLegacyStart; urlForwarding: UrlForwardingStart; embeddable: EmbeddableStart; inspector: InspectorStartContract; navigation: NavigationStart; savedObjectsClient: SavedObjectsClientContract; share?: SharePluginStart; uiActions: UiActionsStart; savedObjects: SavedObjectsStart; } export type RegisterDashboardProviderFn = (provider: DashboardProvider) => void; export interface DashboardSetup { registerDashboardProvider: RegisterDashboardProviderFn; } export interface DashboardStart { getSavedDashboardLoader: () => SavedObjectLoader; addEmbeddableToDashboard: (options: { embeddableId: string; embeddableType: string; }) => void | undefined; dashboardUrlGenerator?: DashboardUrlGenerator; dashboardFeatureFlagConfig: DashboardFeatureFlagConfig; DashboardContainerByValueRenderer: ReturnType; getAttributeService: < A extends { title: string }, V extends EmbeddableInput & { [ATTRIBUTE_SERVICE_KEY]: A } = EmbeddableInput & { [ATTRIBUTE_SERVICE_KEY]: A; }, R extends SavedObjectEmbeddableInput = SavedObjectEmbeddableInput >( type: string, options: AttributeServiceOptions ) => AttributeService; } declare module '../../../plugins/ui_actions/public' { export interface ActionContextMapping { [ACTION_EXPAND_PANEL]: ExpandPanelActionContext; [ACTION_REPLACE_PANEL]: ReplacePanelActionContext; [ACTION_CLONE_PANEL]: ClonePanelActionContext; [ACTION_ADD_TO_LIBRARY]: AddToLibraryActionContext; [ACTION_UNLINK_FROM_LIBRARY]: UnlinkFromLibraryActionContext; [ACTION_LIBRARY_NOTIFICATION]: LibraryNotificationActionContext; } } export class DashboardPlugin implements Plugin { constructor(private initializerContext: PluginInitializerContext) {} private appStateUpdater = new BehaviorSubject(() => ({})); private stopUrlTracking: (() => void) | undefined = undefined; private getActiveUrl: (() => string) | undefined = undefined; private currentHistory: ScopedHistory | undefined = undefined; private dashboardFeatureFlagConfig?: DashboardFeatureFlagConfig; private dashboardProviders: { [key: string]: DashboardProvider } = {}; private dashboardUrlGenerator?: DashboardUrlGenerator; public setup( core: CoreSetup, { share, uiActions, embeddable, home, urlForwarding, data, usageCollection }: SetupDependencies ): DashboardSetup { this.dashboardFeatureFlagConfig = this.initializerContext.config.get< DashboardFeatureFlagConfig >(); const expandPanelAction = new ExpandPanelAction(); uiActions.registerAction(expandPanelAction); uiActions.attachAction(CONTEXT_MENU_TRIGGER, expandPanelAction.id); const startServices = core.getStartServices(); if (share) { this.dashboardUrlGenerator = share.urlGenerators.registerUrlGenerator( createDashboardUrlGenerator(async () => { const [coreStart, , selfStart] = await startServices; return { appBasePath: coreStart.application.getUrlForApp('dashboards'), useHashedUrl: coreStart.uiSettings.get('state:storeInSessionStorage'), savedDashboardLoader: selfStart.getSavedDashboardLoader(), }; }) ); } const getStartServices = async () => { const [coreStart, deps] = await core.getStartServices(); const useHideChrome = ({ toggleChrome } = { toggleChrome: true }) => { React.useEffect(() => { if (toggleChrome) { coreStart.chrome.setIsVisible(false); } return () => { if (toggleChrome) { coreStart.chrome.setIsVisible(true); } }; }, [toggleChrome]); }; const ExitFullScreenButton: React.FC< ExitFullScreenButtonProps & { toggleChrome: boolean; } > = ({ toggleChrome, ...props }) => { useHideChrome({ toggleChrome }); return ; }; return { capabilities: coreStart.application.capabilities, application: coreStart.application, notifications: coreStart.notifications, overlays: coreStart.overlays, embeddable: deps.embeddable, inspector: deps.inspector, SavedObjectFinder: getSavedObjectFinder(coreStart.savedObjects, coreStart.uiSettings), ExitFullScreenButton, uiActions: deps.uiActions, }; }; const factory = new DashboardContainerFactoryDefinition( getStartServices, () => this.currentHistory! ); embeddable.registerEmbeddableFactory(factory.type, factory); const placeholderFactory = new PlaceholderEmbeddableFactory(); embeddable.registerEmbeddableFactory(placeholderFactory.type, placeholderFactory); const { appMounted, appUnMounted, stop: stopUrlTracker, getActiveUrl, restorePreviousUrl, } = createOsdUrlTracker({ baseUrl: core.http.basePath.prepend('/app/dashboards'), defaultSubUrl: `#${DashboardConstants.LANDING_PAGE_PATH}`, storageKey: `lastUrl:${core.http.basePath.get()}:dashboard`, navLinkUpdater$: this.appStateUpdater, toastNotifications: core.notifications.toasts, stateParams: [ { osdUrlKey: '_g', stateUpdate$: data.query.state$.pipe( filter( ({ changes }) => !!(changes.globalFilters || changes.time || changes.refreshInterval) ), map(({ state }) => ({ ...state, filters: state.filters?.filter(opensearchFilters.isFilterPinned), })) ), }, ], getHistory: () => this.currentHistory!, }); this.getActiveUrl = getActiveUrl; this.stopUrlTracking = () => { stopUrlTracker(); }; const registerDashboardProvider: RegisterDashboardProviderFn = ( provider: DashboardProvider ) => { const found = this.dashboardProviders[provider.savedObjectsType]; if (found) { throw new Error(`DashboardProvider ${provider.savedObjectsType} is registered twice`); } if ( isEmpty(provider.createSortText) || isEmpty(provider.createUrl) || isEmpty(provider.createLinkText) ) { throw new Error( `DashboardProvider ${provider.savedObjectsType} requires 'createSortText', 'createLinkText', and 'createUrl'` ); } if (isEmpty(provider.savedObjectsType || isEmpty(provider.savedObjectsName))) { throw new Error( `DashboardProvider ${provider.savedObjectsType} requires 'savedObjectsId', and 'savedObjectsType'` ); } this.dashboardProviders[provider.savedObjectsType] = provider; }; registerDashboardProvider({ savedObjectsType: 'dashboard', savedObjectsName: 'Dashboard', appId: 'dashboard', viewUrlPathFn: (obj) => `#/view/${obj.id}`, editUrlPathFn: (obj) => `/view/${obj.id}?_a=(viewMode:edit)`, createUrl: core.http.basePath.prepend('/app/dashboards#/create'), createSortText: 'Dashboard', createLinkText: ( ), }); const app: App = { id: DashboardConstants.DASHBOARDS_ID, title: 'Dashboards', order: 2500, euiIconType: 'inputOutput', defaultPath: `#${DashboardConstants.LANDING_PAGE_PATH}`, updater$: this.appStateUpdater, category: DEFAULT_APP_CATEGORIES.opensearchDashboards, mount: async (params: AppMountParameters) => { const [coreStart, pluginsStart, dashboardStart] = await core.getStartServices(); this.currentHistory = params.history; // make sure the index pattern list is up to date pluginsStart.data.indexPatterns.clearCache(); // make sure a default index pattern exists // if not, the page will be redirected to management and dashboard won't be rendered await pluginsStart.data.indexPatterns.ensureDefaultIndexPattern(); appMounted(); const { embeddable: embeddableStart, navigation, share: shareStart, data: dataStart, opensearchDashboardsLegacy: { dashboardConfig }, urlForwarding: { navigateToDefaultApp, navigateToLegacyOpenSearchDashboardsUrl }, savedObjects, } = pluginsStart; // dispatch synthetic hash change event to update hash history objects // this is necessary because hash updates triggered by using popState won't trigger this event naturally. const unlistenParentHistory = params.history.listen(() => { window.dispatchEvent(new HashChangeEvent('hashchange')); }); const history = createHashHistory(); // need more research const services: DashboardServices = { ...coreStart, pluginInitializerContext: this.initializerContext, opensearchDashboardsVersion: this.initializerContext.env.packageInfo.version, history, osdUrlStateStorage: createOsdUrlStateStorage({ history, useHash: coreStart.uiSettings.get('state:storeInSessionStorage'), ...withNotifyOnErrors(coreStart.notifications.toasts), }), core: coreStart, dashboardConfig, navigateToDefaultApp, navigateToLegacyOpenSearchDashboardsUrl, navigation, share: shareStart, data: dataStart, savedObjectsClient: coreStart.savedObjects.client, savedDashboards: dashboardStart.getSavedDashboardLoader(), dashboardProviders: () => this.dashboardProviders, chrome: coreStart.chrome, addBasePath: coreStart.http.basePath.prepend, uiSettings: coreStart.uiSettings, savedQueryService: dataStart.query.savedQueries, embeddable: embeddableStart, dashboardCapabilities: coreStart.application.capabilities.dashboard, embeddableCapabilities: { visualizeCapabilities: coreStart.application.capabilities.visualize, mapsCapabilities: coreStart.application.capabilities.maps, }, localStorage: new Storage(localStorage), usageCollection, scopedHistory: params.history, setHeaderActionMenu: params.setHeaderActionMenu, savedObjectsPublic: savedObjects, restorePreviousUrl, toastNotifications: coreStart.notifications.toasts, }; // make sure the index pattern list is up to date await dataStart.indexPatterns.clearCache(); params.element.classList.add('dshAppContainer'); const { renderApp } = await import('./application'); const unmount = renderApp(params, services); return () => { params.element.classList.remove('dshAppContainer'); unlistenParentHistory(); unmount(); appUnMounted(); }; }, }; // TODO: delete this when discover de-angular is completed initAngularBootstrap(); core.application.register(app); urlForwarding.forwardApp( DashboardConstants.DASHBOARDS_ID, DashboardConstants.DASHBOARDS_ID, (path) => { const [, tail] = /(\?.*)/.exec(path) || []; // carry over query if it exists return `#/list${tail || ''}`; } ); urlForwarding.forwardApp( DashboardConstants.DASHBOARD_ID, DashboardConstants.DASHBOARDS_ID, (path) => { const [, id, tail] = /dashboard\/?(.*?)($|\?.*)/.exec(path) || []; if (!id && !tail) { // unrecognized sub url return '#/list'; } if (!id && tail) { // unsaved dashboard, but probably state in URL return `#/create${tail || ''}`; } // persisted dashboard, probably with url state return `#/view/${id}${tail || ''}`; } ); if (home) { home.featureCatalogue.register({ id: DashboardConstants.DASHBOARD_ID, title: i18n.translate('dashboard.featureCatalogue.dashboardTitle', { defaultMessage: 'Dashboard', }), subtitle: i18n.translate('dashboard.featureCatalogue.dashboardSubtitle', { defaultMessage: 'Analyze data in dashboards.', }), description: i18n.translate('dashboard.featureCatalogue.dashboardDescription', { defaultMessage: 'Display and share a collection of visualizations and saved searches.', }), icon: 'dashboardApp', path: `/app/dashboards#${DashboardConstants.LANDING_PAGE_PATH}`, showOnHomePage: false, category: FeatureCatalogueCategory.DATA, solutionId: 'opensearchDashboards', order: 100, }); } return { registerDashboardProvider, }; } private addEmbeddableToDashboard( core: CoreStart, { embeddableId, embeddableType }: { embeddableId: string; embeddableType: string } ) { if (!this.getActiveUrl) { throw new Error('dashboard is not ready yet.'); } const lastDashboardUrl = this.getActiveUrl(); const dashboardUrl = addEmbeddableToDashboardUrl( lastDashboardUrl, embeddableId, embeddableType ); core.application.navigateToApp('dashboards', { path: dashboardUrl }); } public start(core: CoreStart, plugins: StartDependencies): DashboardStart { const { notifications } = core; const { uiActions, data: { indexPatterns, search }, embeddable, } = plugins; const SavedObjectFinder = getSavedObjectFinder(core.savedObjects, core.uiSettings); const changeViewAction = new ReplacePanelAction( core, SavedObjectFinder, notifications, plugins.embeddable.getEmbeddableFactories ); uiActions.registerAction(changeViewAction); uiActions.attachAction(CONTEXT_MENU_TRIGGER, changeViewAction.id); const clonePanelAction = new ClonePanelAction(core); uiActions.registerAction(clonePanelAction); uiActions.attachAction(CONTEXT_MENU_TRIGGER, clonePanelAction.id); if (this.dashboardFeatureFlagConfig?.allowByValueEmbeddables) { const addToLibraryAction = new AddToLibraryAction(); uiActions.registerAction(addToLibraryAction); uiActions.attachAction(CONTEXT_MENU_TRIGGER, addToLibraryAction.id); const unlinkFromLibraryAction = new UnlinkFromLibraryAction(); uiActions.registerAction(unlinkFromLibraryAction); uiActions.attachAction(CONTEXT_MENU_TRIGGER, unlinkFromLibraryAction.id); const libraryNotificationAction = new LibraryNotificationAction(); uiActions.registerAction(libraryNotificationAction); uiActions.attachAction(PANEL_NOTIFICATION_TRIGGER, libraryNotificationAction.id); } const savedDashboardLoader = createSavedDashboardLoader({ savedObjectsClient: core.savedObjects.client, indexPatterns, search, chrome: core.chrome, overlays: core.overlays, }); const dashboardContainerFactory = plugins.embeddable.getEmbeddableFactory( DASHBOARD_CONTAINER_TYPE )! as DashboardContainerFactory; return { getSavedDashboardLoader: () => savedDashboardLoader, addEmbeddableToDashboard: this.addEmbeddableToDashboard.bind(this, core), dashboardUrlGenerator: this.dashboardUrlGenerator, dashboardFeatureFlagConfig: this.dashboardFeatureFlagConfig!, DashboardContainerByValueRenderer: createDashboardContainerByValueRenderer({ factory: dashboardContainerFactory, }), getAttributeService: (type: string, options) => new AttributeService( type, showSaveModal, core.i18n.Context, core.notifications.toasts, options, embeddable.getEmbeddableFactory ), }; } public stop() { if (this.stopUrlTracking) { this.stopUrlTracking(); } } }