From 65d57f7b07011d60771da410a00f94320f222dce Mon Sep 17 00:00:00 2001 From: "Qingyang(Abby) Hu" Date: Wed, 7 Jun 2023 11:56:17 -0700 Subject: [PATCH] Basic top nav bar for dashboard (#4108) Basic top nav bar for dashboard This PR will add basic structure to render top nav bar, including a basic implementation for dashboard app state. This is not functionality complete, but to help implement a basic working dashboard app as the first step. Signed-off-by: abbyhu2000 --- .../components/dashboard_editor.tsx | 208 +++++++++++++++++- .../components/dashboard_top_nav.tsx | 113 +++++++++- .../utils/create_dashboard_app_state.tsx | 117 ++++++++++ .../utils/use/use_chrome_visibility.ts | 27 +++ .../utils/use/use_dashboard_app_state.tsx | 88 ++++++++ .../utils/use/use_saved_dashboard_instance.ts | 120 ++++++++++ .../public/application/utils/utils.ts | 29 +++ src/plugins/dashboard/public/types.ts | 13 +- 8 files changed, 709 insertions(+), 6 deletions(-) create mode 100644 src/plugins/dashboard/public/application/utils/create_dashboard_app_state.tsx create mode 100644 src/plugins/dashboard/public/application/utils/use/use_chrome_visibility.ts create mode 100644 src/plugins/dashboard/public/application/utils/use/use_dashboard_app_state.tsx create mode 100644 src/plugins/dashboard/public/application/utils/use/use_saved_dashboard_instance.ts create mode 100644 src/plugins/dashboard/public/application/utils/utils.ts diff --git a/src/plugins/dashboard/public/application/components/dashboard_editor.tsx b/src/plugins/dashboard/public/application/components/dashboard_editor.tsx index d3d0814f0c4a..2be15917e940 100644 --- a/src/plugins/dashboard/public/application/components/dashboard_editor.tsx +++ b/src/plugins/dashboard/public/application/components/dashboard_editor.tsx @@ -3,8 +3,212 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; +import React, { useEffect, useState } from 'react'; +import { useParams } from 'react-router-dom'; +import { EventEmitter } from 'events'; +import { EMPTY, Subscription, merge } from 'rxjs'; +import { catchError, distinctUntilChanged, map, mapTo, startWith, switchMap } from 'rxjs/operators'; +import deepEqual from 'fast-deep-equal'; +import { DashboardTopNav } from '../components/dashboard_top_nav'; +import { useChromeVisibility } from '../utils/use/use_chrome_visibility'; +import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; +import { useSavedDashboardInstance } from '../utils/use/use_saved_dashboard_instance'; + +import { DashboardServices, SavedDashboardPanel } from '../../types'; +import { + DASHBOARD_CONTAINER_TYPE, + DashboardContainer, + DashboardContainerInput, + DashboardPanelState, +} from '../embeddable'; +import { + ContainerOutput, + ErrorEmbeddable, + ViewMode, + isErrorEmbeddable, +} from '../../embeddable_plugin'; +import { DashboardEmptyScreen, DashboardEmptyScreenProps } from '../dashboard_empty_screen'; +import { convertSavedDashboardPanelToPanelState } from '../lib/embeddable_saved_object_converters'; +import { useDashboardAppState } from '../utils/use/use_dashboard_app_state'; export const DashboardEditor = () => { - return
Dashboard Editor
; + const { id: dashboardIdFromUrl } = useParams<{ id: string }>(); + const { services } = useOpenSearchDashboards(); + const { embeddable, data, dashboardConfig, embeddableCapabilities, uiSettings, http } = services; + const { query: queryService } = data; + const { visualizeCapabilities, mapsCapabilities } = embeddableCapabilities; + const timefilter = queryService.timefilter.timefilter; + const isChromeVisible = useChromeVisibility(services.chrome); + const [eventEmitter] = useState(new EventEmitter()); + + const { savedDashboardInstance } = useSavedDashboardInstance( + services, + eventEmitter, + isChromeVisible, + dashboardIdFromUrl + ); + + const { appState } = useDashboardAppState(services, eventEmitter, savedDashboardInstance); + + const appStateData = appState?.get(); + if (!appStateData) { + return null; + } + let dashboardContainer: DashboardContainer | undefined; + let inputSubscription: Subscription | undefined; + let outputSubscription: Subscription | undefined; + + const dashboardDom = document.getElementById('dashboardViewport'); + const dashboardFactory = embeddable.getEmbeddableFactory< + DashboardContainerInput, + ContainerOutput, + DashboardContainer + >(DASHBOARD_CONTAINER_TYPE); + + const getShouldShowEditHelp = () => { + return ( + !appStateData.panels.length && + appStateData.viewMode === ViewMode.EDIT && + !dashboardConfig.getHideWriteControls() + ); + }; + + const getShouldShowViewHelp = () => { + return ( + !appStateData.panels.length && + appStateData.viewMode === ViewMode.VIEW && + !dashboardConfig.getHideWriteControls() + ); + }; + + const shouldShowUnauthorizedEmptyState = () => { + const readonlyMode = + !appStateData.panels.length && + !getShouldShowEditHelp() && + !getShouldShowViewHelp() && + dashboardConfig.getHideWriteControls(); + const userHasNoPermissions = + !appStateData.panels.length && !visualizeCapabilities.save && !mapsCapabilities.save; + return readonlyMode || userHasNoPermissions; + }; + + const getEmptyScreenProps = ( + shouldShowEditHelp: boolean, + isEmptyInReadOnlyMode: boolean + ): DashboardEmptyScreenProps => { + const emptyScreenProps: DashboardEmptyScreenProps = { + onLinkClick: () => {}, // TODO + showLinkToVisualize: shouldShowEditHelp, + uiSettings, + http, + }; + if (shouldShowEditHelp) { + emptyScreenProps.onVisualizeClick = () => { + alert('click'); // TODO + }; + } + if (isEmptyInReadOnlyMode) { + emptyScreenProps.isReadonlyMode = true; + } + return emptyScreenProps; + }; + + const getDashboardInput = () => { + const embeddablesMap: { + [key: string]: DashboardPanelState; + } = {}; + appStateData.panels.forEach((panel: SavedDashboardPanel) => { + embeddablesMap[panel.panelIndex] = convertSavedDashboardPanelToPanelState(panel); + }); + + const lastReloadRequestTime = 0; + return { + id: savedDashboardInstance.id || '', + filters: appStateData.filters, + hidePanelTitles: appStateData?.options.hidePanelTitles, + query: appStateData.query, + timeRange: { + ..._.cloneDeep(timefilter.getTime()), + }, + refreshConfig: timefilter.getRefreshInterval(), + viewMode: appStateData.viewMode, + panels: embeddablesMap, + isFullScreenMode: appStateData?.fullScreenMode, + isEmbeddedExternally: false, // TODO + // isEmptyState: shouldShowEditHelp || shouldShowViewHelp || isEmptyInReadonlyMode, + isEmptyState: false, // TODO + useMargins: appStateData.options.useMargins, + lastReloadRequestTime, // TODO + title: appStateData.title, + description: appStateData.description, + expandedPanelId: appStateData.expandedPanelId, + }; + }; + + if (dashboardFactory) { + dashboardFactory + .create(getDashboardInput()) + .then((container: DashboardContainer | ErrorEmbeddable | undefined) => { + if (container && !isErrorEmbeddable(container)) { + dashboardContainer = container; + + dashboardContainer.renderEmpty = () => { + const shouldShowEditHelp = getShouldShowEditHelp(); + const shouldShowViewHelp = getShouldShowViewHelp(); + const isEmptyInReadOnlyMode = shouldShowUnauthorizedEmptyState(); + const isEmptyState = shouldShowEditHelp || shouldShowViewHelp || isEmptyInReadOnlyMode; + return isEmptyState ? ( + + ) : null; + }; + + outputSubscription = merge( + // output of dashboard container itself + dashboardContainer.getOutput$(), + // plus output of dashboard container children, + // children may change, so make sure we subscribe/unsubscribe with switchMap + dashboardContainer.getOutput$().pipe( + map(() => dashboardContainer!.getChildIds()), + distinctUntilChanged(deepEqual), + switchMap((newChildIds: string[]) => + merge( + ...newChildIds.map((childId) => + dashboardContainer! + .getChild(childId) + .getOutput$() + .pipe(catchError(() => EMPTY)) + ) + ) + ) + ) + ) + .pipe( + mapTo(dashboardContainer), + startWith(dashboardContainer) // to trigger initial index pattern update + // updateIndexPatternsOperator //TODO + ) + .subscribe(); + + inputSubscription = dashboardContainer.getInput$().subscribe(() => {}); + + if (dashboardDom && container) { + container.render(dashboardDom); + } + } + }); + } + + return ( +
+ {savedDashboardInstance && appState && ( + + )} +
+ ); }; diff --git a/src/plugins/dashboard/public/application/components/dashboard_top_nav.tsx b/src/plugins/dashboard/public/application/components/dashboard_top_nav.tsx index 1603fb89dad5..4ee92567c590 100644 --- a/src/plugins/dashboard/public/application/components/dashboard_top_nav.tsx +++ b/src/plugins/dashboard/public/application/components/dashboard_top_nav.tsx @@ -3,8 +3,115 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; +import React, { memo, useState, useEffect } from 'react'; +import { Filter } from 'src/plugins/data/public'; +import { useCallback } from 'react'; +import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; +import { getTopNavConfig } from '../top_nav/get_top_nav_config'; +import { DashboardAppState, DashboardServices, NavAction } from '../../types'; -export const DashboardTopNav = () => { - return
Dashboard Top Nav
; +interface DashboardTopNavProps { + isChromeVisible: boolean; + savedDashboardInstance: any; + appState: DashboardAppState; +} + +enum UrlParams { + SHOW_TOP_MENU = 'show-top-menu', + SHOW_QUERY_INPUT = 'show-query-input', + SHOW_TIME_FILTER = 'show-time-filter', + SHOW_FILTER_BAR = 'show-filter-bar', + HIDE_FILTER_BAR = 'hide-filter-bar', +} + +const TopNav = ({ isChromeVisible, savedDashboardInstance, appState }: DashboardTopNavProps) => { + const [filters, setFilters] = useState([]); + const [topNavMenu, setTopNavMenu] = useState(); + const [isFullScreenMode, setIsFullScreenMode] = useState(); + + const { services } = useOpenSearchDashboards(); + const { TopNavMenu } = services.navigation.ui; + const { data, dashboardConfig, setHeaderActionMenu } = services; + const { query: queryService } = data; + + // TODO: this should base on URL + const isEmbeddedExternally = false; + + // TODO: should use URL params + const shouldForceDisplay = (param: string): boolean => { + // const [searchParams] = useSearchParams(); + return false; + }; + + const shouldShowNavBarComponent = (forceShow: boolean): boolean => + (forceShow || isChromeVisible) && !appState?.fullScreenMode; + + useEffect(() => { + setFilters(queryService.filterManager.getFilters()); + }, [services, queryService]); + + useEffect(() => { + const navActions: { + [key: string]: NavAction; + } = {}; // TODO: need to implement nav actions + setTopNavMenu( + getTopNavConfig(appState?.viewMode, navActions, dashboardConfig.getHideWriteControls()) + ); + }, [appState, services, dashboardConfig]); + + useEffect(() => { + setIsFullScreenMode(appState?.fullScreenMode); + }, [appState, services]); + + const shouldShowFilterBar = (forceHide: boolean): boolean => + !forceHide && (filters!.length > 0 || !appState?.fullScreenMode); + + const forceShowTopNavMenu = shouldForceDisplay(UrlParams.SHOW_TOP_MENU); + const forceShowQueryInput = shouldForceDisplay(UrlParams.SHOW_QUERY_INPUT); + const forceShowDatePicker = shouldForceDisplay(UrlParams.SHOW_TIME_FILTER); + const forceHideFilterBar = shouldForceDisplay(UrlParams.HIDE_FILTER_BAR); + const showTopNavMenu = shouldShowNavBarComponent(forceShowTopNavMenu); + const showQueryInput = shouldShowNavBarComponent(forceShowQueryInput); + const showDatePicker = shouldShowNavBarComponent(forceShowDatePicker); + const showQueryBar = showQueryInput || showDatePicker; + const showFilterBar = shouldShowFilterBar(forceHideFilterBar); + const showSearchBar = showQueryBar || showFilterBar; + + // TODO: implement handleRefresh + const handleRefresh = useCallback((_payload: any, isUpdate?: boolean) => { + /* if (isUpdate === false) { + // The user can still request a reload in the query bar, even if the + // query is the same, and in that case, we have to explicitly ask for + // a reload, since no state changes will cause it. + lastReloadRequestTime = new Date().getTime(); + const changes = getChangesFromAppStateForContainerState(); + if (changes && dashboardContainer) { + dashboardContainer.updateInput(changes); + }*/ + }, []); + + return isChromeVisible ? ( + {}} + onQuerySubmit={handleRefresh} + setMenuMountPoint={isEmbeddedExternally ? undefined : setHeaderActionMenu} + /> + ) : null; }; + +export const DashboardTopNav = memo(TopNav); diff --git a/src/plugins/dashboard/public/application/utils/create_dashboard_app_state.tsx b/src/plugins/dashboard/public/application/utils/create_dashboard_app_state.tsx new file mode 100644 index 000000000000..e4c0d9448a2d --- /dev/null +++ b/src/plugins/dashboard/public/application/utils/create_dashboard_app_state.tsx @@ -0,0 +1,117 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { migrateAppState } from '../lib/migrate_app_state'; +import { + IOsdUrlStateStorage, + createStateContainer, + syncState, +} from '../../../../opensearch_dashboards_utils/public'; +import { + DashboardAppState, + DashboardAppStateTransitions, + DashboardAppStateInUrl, + DashboardServices, +} from '../../types'; +import { ViewMode } from '../../embeddable_plugin'; +import { getDashboardIdFromUrl } from '../lib'; + +const STATE_STORAGE_KEY = '_a'; + +interface Arguments { + osdUrlStateStorage: IOsdUrlStateStorage; + stateDefaults: DashboardAppState; + services: DashboardServices; + instance: any; +} + +export const createDashboardAppState = ({ + stateDefaults, + osdUrlStateStorage, + services, + instance, +}: Arguments) => { + const urlState = osdUrlStateStorage.get(STATE_STORAGE_KEY); + const { opensearchDashboardsVersion, usageCollection, history } = services; + const initialState = migrateAppState( + { + ...stateDefaults, + ...urlState, + }, + opensearchDashboardsVersion, + usageCollection + ); + + const pureTransitions = { + set: (state) => (prop, value) => ({ ...state, [prop]: value }), + setOption: (state) => (option, value) => ({ + ...state, + options: { + ...state.options, + [option]: value, + }, + }), + } as DashboardAppStateTransitions; + /* + make sure url ('_a') matches initial state + Initializing appState does two things - first it translates the defaults into AppState, + second it updates appState based on the url (the url trumps the defaults). This means if + we update the state format at all and want to handle BWC, we must not only migrate the + data stored with saved vis, but also any old state in the url. + */ + osdUrlStateStorage.set(STATE_STORAGE_KEY, initialState, { replace: true }); + + const stateContainer = createStateContainer( + initialState, + pureTransitions + ); + + const toUrlState = (state: DashboardAppState): DashboardAppStateInUrl => { + if (state.viewMode === ViewMode.VIEW) { + const { panels, ...stateWithoutPanels } = state; + return stateWithoutPanels; + } + return state; + }; + + const { start: startStateSync, stop: stopStateSync } = syncState({ + storageKey: STATE_STORAGE_KEY, + stateContainer: { + ...stateContainer, + get: () => toUrlState(stateContainer.get()), + set: (state: DashboardAppStateInUrl | null) => { + // sync state required state container to be able to handle null + // overriding set() so it could handle null coming from url + if (state) { + // Skip this update if current dashboardId in the url is different from what we have in the current instance of state manager + // As dashboard is driven by angular at the moment, the destroy cycle happens async, + // If the dashboardId has changed it means this instance + // is going to be destroyed soon and we shouldn't sync state anymore, + // as it could potentially trigger further url updates + const currentDashboardIdInUrl = getDashboardIdFromUrl(history.location.pathname); + if (currentDashboardIdInUrl !== instance.id) return; + + stateContainer.set({ + ...stateDefaults, + ...state, + }); + } else { + // Do nothing in case when state from url is empty, + // this fixes: https://github.com/elastic/kibana/issues/57789 + // There are not much cases when state in url could become empty: + // 1. User manually removed `_a` from the url + // 2. Browser is navigating away from the page and most likely there is no `_a` in the url. + // In this case we don't want to do any state updates + // and just allow $scope.$on('destroy') fire later and clean up everything + } + }, + }, + stateStorage: osdUrlStateStorage, + }); + + // start syncing the appState with the ('_a') url + startStateSync(); + return { stateContainer, stopStateSync }; +}; diff --git a/src/plugins/dashboard/public/application/utils/use/use_chrome_visibility.ts b/src/plugins/dashboard/public/application/utils/use/use_chrome_visibility.ts new file mode 100644 index 000000000000..7abb5a6d355a --- /dev/null +++ b/src/plugins/dashboard/public/application/utils/use/use_chrome_visibility.ts @@ -0,0 +1,27 @@ +/* + * 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. + */ + +import { useState, useEffect } from 'react'; +import { ChromeStart } from 'opensearch-dashboards/public'; + +export const useChromeVisibility = (chrome: ChromeStart) => { + const [isVisible, setIsVisible] = useState(true); + + useEffect(() => { + const subscription = chrome.getIsVisible$().subscribe((value: boolean) => { + setIsVisible(value); + }); + + return () => subscription.unsubscribe(); + }, [chrome]); + + return isVisible; +}; diff --git a/src/plugins/dashboard/public/application/utils/use/use_dashboard_app_state.tsx b/src/plugins/dashboard/public/application/utils/use/use_dashboard_app_state.tsx new file mode 100644 index 000000000000..e14e790125f0 --- /dev/null +++ b/src/plugins/dashboard/public/application/utils/use/use_dashboard_app_state.tsx @@ -0,0 +1,88 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import EventEmitter from 'events'; +import { useEffect, useState } from 'react'; +import { cloneDeep } from 'lodash'; +import { map } from 'rxjs/operators'; +import { connectToQueryState, opensearchFilters } from '../../../../../data/public'; +import { migrateLegacyQuery } from '../../lib/migrate_legacy_query'; +import { DashboardServices } from '../../../types'; + +import { DashboardAppStateContainer } from '../../../types'; +import { migrateAppState, getAppStateDefaults } from '../../lib'; +import { createDashboardAppState } from '../create_dashboard_app_state'; + +/** + * This effect is responsible for instantiating the dashboard app state container, + * which is in sync with "_a" url param + */ +export const useDashboardAppState = ( + services: DashboardServices, + eventEmitter: EventEmitter, + instance: any +) => { + const [appState, setAppState] = useState(null); + + useEffect(() => { + if (!instance) { + return; + } + const { dashboardConfig, usageCollection, opensearchDashboardsVersion } = services; + const hideWriteControls = dashboardConfig.getHideWriteControls(); + const stateDefaults = migrateAppState( + getAppStateDefaults(instance, hideWriteControls), + opensearchDashboardsVersion, + usageCollection + ); + + const { stateContainer, stopStateSync } = createDashboardAppState({ + stateDefaults, + osdUrlStateStorage: services.osdUrlStateStorage, + services, + instance, + }); + + const { filterManager, queryString } = services.data.query; + + // sync initial app state from state container to managers + filterManager.setAppFilters(cloneDeep(stateContainer.getState().filters)); + queryString.setQuery(migrateLegacyQuery(stateContainer.getState().query)); + + // setup syncing of app filters between app state and query services + const stopSyncingAppFilters = connectToQueryState( + services.data.query, + { + set: ({ filters, query }) => { + stateContainer.transitions.set('filters', filters || []); + stateContainer.transitions.set('query', query || queryString.getDefaultQuery()); + }, + get: () => ({ + filters: stateContainer.getState().filters, + query: migrateLegacyQuery(stateContainer.getState().query), + }), + state$: stateContainer.state$.pipe( + map((state) => ({ + filters: state.filters, + query: queryString.formatQuery(state.query), + })) + ), + }, + { + filters: opensearchFilters.FilterStateStore.APP_STATE, + query: true, + } + ); + + setAppState(stateContainer); + + return () => { + stopStateSync(); + stopSyncingAppFilters(); + }; + }, [eventEmitter, instance, services]); + + return { appState }; +}; diff --git a/src/plugins/dashboard/public/application/utils/use/use_saved_dashboard_instance.ts b/src/plugins/dashboard/public/application/utils/use/use_saved_dashboard_instance.ts new file mode 100644 index 000000000000..e7e1633ac41b --- /dev/null +++ b/src/plugins/dashboard/public/application/utils/use/use_saved_dashboard_instance.ts @@ -0,0 +1,120 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { i18n } from '@osd/i18n'; +import { EventEmitter } from 'events'; +import { useEffect, useRef, useState } from 'react'; +import { + redirectWhenMissing, + SavedObjectNotFound, +} from '../../../../../opensearch_dashboards_utils/public'; +import { DashboardConstants } from '../../../dashboard_constants'; +import { DashboardServices } from '../../../types'; + +/** + * This effect is responsible for instantiating a saved dashboard or creating a new one + * using url parameters, embedding and destroying it in DOM + */ +export const useSavedDashboardInstance = ( + services: DashboardServices, + eventEmitter: EventEmitter, + isChromeVisible: boolean | undefined, + dashboardIdFromUrl: string | undefined +) => { + const [state, setState] = useState<{ + savedDashboardInstance?: any; + }>({}); + + const dashboardId = useRef(''); + + useEffect(() => { + const getSavedDashboardInstance = async () => { + const { + application: { navigateToApp }, + chrome, + history, + http: { basePath }, + notifications, + savedDashboards, + } = services; + try { + console.log('trying to get saved dashboard'); + let savedDashboardInstance: any; + if (history.location.pathname === '/create') { + try { + savedDashboardInstance = await savedDashboards.get(); + } catch { + redirectWhenMissing({ + history, + basePath, + navigateToApp, + mapping: { + dashboard: DashboardConstants.LANDING_PAGE_PATH, + }, + toastNotifications: notifications.toasts, + }); + } + } else if (dashboardIdFromUrl) { + try { + savedDashboardInstance = await savedDashboards.get(dashboardIdFromUrl); + chrome.recentlyAccessed.add( + savedDashboardInstance.getFullPath(), + savedDashboardInstance.title, + dashboardIdFromUrl + ); + console.log('saved dashboard', savedDashboardInstance); + } catch (error) { + // Preserve BWC of v5.3.0 links for new, unsaved dashboards. + // See https://github.com/elastic/kibana/issues/10951 for more context. + if (error instanceof SavedObjectNotFound && dashboardIdFromUrl === 'create') { + // Note preserve querystring part is necessary so the state is preserved through the redirect. + history.replace({ + ...history.location, // preserve query, + pathname: DashboardConstants.CREATE_NEW_DASHBOARD_URL, + }); + + notifications.toasts.addWarning( + i18n.translate('dashboard.urlWasRemovedInSixZeroWarningMessage', { + defaultMessage: + 'The url "dashboard/create" was removed in 6.0. Please update your bookmarks.', + }) + ); + return new Promise(() => {}); + } else { + // E.g. a corrupt or deleted dashboard + notifications.toasts.addDanger(error.message); + history.push(DashboardConstants.LANDING_PAGE_PATH); + return new Promise(() => {}); + } + } + } + + setState({ savedDashboardInstance }); + } catch (error) {} + }; + + if (isChromeVisible === undefined) { + // waiting for specifying chrome + return; + } + + if (!dashboardId.current) { + dashboardId.current = dashboardIdFromUrl || 'new'; + getSavedDashboardInstance(); + } else if ( + dashboardIdFromUrl && + dashboardId.current !== dashboardIdFromUrl && + state.savedDashboardInstance?.id !== dashboardIdFromUrl + ) { + dashboardId.current = dashboardIdFromUrl; + setState({}); + getSavedDashboardInstance(); + } + }, [eventEmitter, isChromeVisible, services, state.savedDashboardInstance, dashboardIdFromUrl]); + + return { + ...state, + }; +}; diff --git a/src/plugins/dashboard/public/application/utils/utils.ts b/src/plugins/dashboard/public/application/utils/utils.ts new file mode 100644 index 000000000000..9a337585dec0 --- /dev/null +++ b/src/plugins/dashboard/public/application/utils/utils.ts @@ -0,0 +1,29 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Filter } from 'src/plugins/data/public'; +import { DashboardServices } from '../../types'; + +export const getDefaultQuery = ({ data }: DashboardServices) => { + return data.query.queryString.getDefaultQuery(); +}; + +export const dashboardStateToEditorState = ( + dashboardInstance: any, + services: DashboardServices +) => { + const savedDashboardState = { + id: dashboardInstance.id, + title: dashboardInstance.title, + description: dashboardInstance.description, + searchSource: dashboardInstance.searchSource, + savedSearchId: dashboardInstance.savedSearchId, + }; + return { + query: dashboardInstance.searchSource?.getOwnField('query') || getDefaultQuery(services), + filters: (dashboardInstance.searchSource?.getOwnField('filter') as Filter[]) || [], + savedDashboardState, + }; +}; diff --git a/src/plugins/dashboard/public/types.ts b/src/plugins/dashboard/public/types.ts index 6ba53fe9e050..8a94a424b940 100644 --- a/src/plugins/dashboard/public/types.ts +++ b/src/plugins/dashboard/public/types.ts @@ -40,7 +40,11 @@ import { ScopedHistory, AppMountParameters, } from 'src/core/public'; -import { IOsdUrlStateStorage, Storage } from 'src/plugins/opensearch_dashboards_utils/public'; +import { + IOsdUrlStateStorage, + ReduxLikeStateContainer, + Storage, +} from 'src/plugins/opensearch_dashboards_utils/public'; import { SavedObjectLoader, SavedObjectsStart } from 'src/plugins/saved_objects/public'; import { OpenSearchDashboardsLegacyStart } from 'src/plugins/opensearch_dashboards_legacy/public'; import { SharePluginStart } from 'src/plugins/share/public'; @@ -54,6 +58,8 @@ import { SavedDashboardPanel730ToLatest } from '../common'; export interface DashboardCapabilities { showWriteControls: boolean; createNew: boolean; + showSavedQuery: boolean; + saveQuery: boolean; } // TODO: Replace Saved object interfaces by the ones Core will provide when it is ready. @@ -149,6 +155,11 @@ export interface DashboardAppStateTransitions { ) => DashboardAppState; } +export type DashboardAppStateContainer = ReduxLikeStateContainer< + DashboardAppState, + DashboardAppStateTransitions +>; + export interface SavedDashboardPanelMap { [key: string]: SavedDashboardPanel; }