From 2a535c8e27b76ab7289f7d73e23463ce4eabca33 Mon Sep 17 00:00:00 2001 From: abbyhu2000 Date: Wed, 31 May 2023 22:48:53 +0000 Subject: [PATCH] Render editor page with basic nav actions Added dashboard embeddable container to render the dashboard editor page. Signed-off-by: abbyhu2000 --- .../components/dashboard_editor.tsx | 228 +++--------- .../components/dashboard_top_nav.tsx | 37 +- .../utils/dashboard_embeddable_editor.tsx | 44 +++ .../utils/use/use_dashboard_app_state.tsx | 98 +++--- .../utils/use/use_dashboard_container.tsx | 326 ++++++++++++++++++ .../utils/use/use_editor_updates.ts | 66 ++++ .../utils/use/use_saved_dashboard_instance.ts | 36 +- src/plugins/dashboard/public/types.ts | 5 + .../utils/use/use_visualize_app_state.tsx | 1 + 9 files changed, 574 insertions(+), 267 deletions(-) create mode 100644 src/plugins/dashboard/public/application/utils/dashboard_embeddable_editor.tsx create mode 100644 src/plugins/dashboard/public/application/utils/use/use_dashboard_container.tsx create mode 100644 src/plugins/dashboard/public/application/utils/use/use_editor_updates.ts diff --git a/src/plugins/dashboard/public/application/components/dashboard_editor.tsx b/src/plugins/dashboard/public/application/components/dashboard_editor.tsx index 32b5d7f7ceb2..9b78065b5c79 100644 --- a/src/plugins/dashboard/public/application/components/dashboard_editor.tsx +++ b/src/plugins/dashboard/public/application/components/dashboard_editor.tsx @@ -6,42 +6,24 @@ 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 { DashboardServices } from '../../types'; import { useDashboardAppState } from '../utils/use/use_dashboard_app_state'; +import { useDashboardContainer } from '../utils/use/use_dashboard_container'; +import { useEditorUpdates } from '../utils/use/use_editor_updates'; export const DashboardEditor = () => { 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 dashboardDom = document.getElementById('dashboardViewport'); - const { savedDashboardInstance } = useSavedDashboardInstance( + const savedDashboardInstance = useSavedDashboardInstance( services, eventEmitter, isChromeVisible, @@ -50,176 +32,50 @@ export const DashboardEditor = () => { 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 [dashboardContainer, setDashboardContainer] = useState({}); - 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 { dashboardContainer } = useDashboardContainer( + services, + isChromeVisible, + eventEmitter, + dashboardDom, + savedDashboardInstance, + appState + ); - const getDashboardInput = () => { - const embeddablesMap: { - [key: string]: DashboardPanelState; - } = {}; - appStateData.panels.forEach((panel: SavedDashboardPanel) => { - embeddablesMap[panel.panelIndex] = convertSavedDashboardPanelToPanelState(panel); - }); + const { isEmbeddableRendered, currentAppState } = useEditorUpdates( + services, + eventEmitter, + savedDashboardInstance, + dashboardContainer, + appState + ); - 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, - }; - }; useEffect(() => { - if (dashboardFactory) { - dashboardFactory - .create(getDashboardInput()) - .then((container: DashboardContainer | ErrorEmbeddable | undefined) => { - if (container && !isErrorEmbeddable(container)) { - // dashboardContainer = container; - setDashboardContainer(container); - - } - }); - } - }, [dashboardFactory, getDashboardInput]); - - 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((foo) => { - console.log(foo); - }); - - if (dashboardDom && dashboardContainer) { - console.log('dashboard container inside', dashboardContainer); - dashboardContainer.render(dashboardDom); - } + // clean up all registered listeners if any is left + return () => { + eventEmitter.removeAllListeners(); + }; + }, [eventEmitter]); + console.log('savedDashboardInstance', savedDashboardInstance); + console.log('appState', appState); + console.log('currentAppState', currentAppState); + console.log('isEmbeddableRendered', isEmbeddableRendered); + console.log('dashboardContainer', dashboardContainer); return (
- {savedDashboardInstance && appState && ( - - )} +
+ {savedDashboardInstance && appState && dashboardContainer && currentAppState && ( + + )} +
); }; 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 85adaef917d7..f79cf76d87d2 100644 --- a/src/plugins/dashboard/public/application/components/dashboard_top_nav.tsx +++ b/src/plugins/dashboard/public/application/components/dashboard_top_nav.tsx @@ -20,8 +20,9 @@ import { DashboardContainer } from '../embeddable'; interface DashboardTopNavProps { isChromeVisible: boolean; savedDashboardInstance: any; - currentAppState: DashboardAppState; stateContainer: DashboardAppStateContainer; + currentAppState: DashboardAppState; + isEmbeddableRendered: boolean; dashboardContainer?: DashboardContainer; } @@ -36,8 +37,9 @@ enum UrlParams { const TopNav = ({ isChromeVisible, savedDashboardInstance, - currentAppState, stateContainer, + currentAppState, + isEmbeddableRendered, dashboardContainer, }: DashboardTopNavProps) => { const [filters, setFilters] = useState([]); @@ -66,15 +68,21 @@ const TopNav = ({ }, [services, queryService]); useEffect(() => { - const navActions = getNavActions( - stateContainer, - savedDashboardInstance, - services, - dashboardContainer - ); - setTopNavMenu( - getTopNavConfig(currentAppState?.viewMode, navActions, dashboardConfig.getHideWriteControls()) - ); + if (isEmbeddableRendered) { + const navActions = getNavActions( + stateContainer, + savedDashboardInstance, + services, + dashboardContainer + ); + setTopNavMenu( + getTopNavConfig( + currentAppState?.viewMode, + navActions, + dashboardConfig.getHideWriteControls() + ) + ); + } }, [ currentAppState, services, @@ -82,6 +90,7 @@ const TopNav = ({ dashboardContainer, savedDashboardInstance, stateContainer, + isEmbeddableRendered, ]); useEffect(() => { @@ -115,9 +124,9 @@ const TopNav = ({ }*/ }, []); - console.log('currentAppState', currentAppState); - console.log('state container get()', stateContainer.get()); - console.log('dashboard container top nav', dashboardContainer); + // console.log('currentAppState', currentAppState); + // console.log('state container get()', stateContainer.get()); + // console.log('dashboard container top nav', dashboardContainer); return isChromeVisible ? ( { + if (!dom) { + return; + } + + dashboardContainer.render(dom); + setTimeout(() => { + eventEmitter.emit('embeddableRendered'); + }); + + return () => dashboardContainer.destroy(); + }, [dashboardContainer, eventEmitter, dom]); + + useEffect(() => { + dashboardContainer.updateInput({ + timeRange, + filters, + query, + }); + }, [dashboardContainer, timeRange, filters, query]); + + return
; +} + +// default export required for React.Lazy +// eslint-disable-next-line import/no-default-export +export { DashboardEmbeddableEditor as default }; 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 index e14e790125f0..464febb05c60 100644 --- 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 @@ -24,64 +24,64 @@ export const useDashboardAppState = ( eventEmitter: EventEmitter, instance: any ) => { - const [appState, setAppState] = useState(null); + const [appState, setAppState] = useState(); useEffect(() => { - if (!instance) { - return; - } - const { dashboardConfig, usageCollection, opensearchDashboardsVersion } = services; - const hideWriteControls = dashboardConfig.getHideWriteControls(); - const stateDefaults = migrateAppState( - getAppStateDefaults(instance, hideWriteControls), - opensearchDashboardsVersion, - usageCollection - ); + if (instance) { + // const savedDashboardInstance = ('savedDashboardInstance' in instance) + 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 { stateContainer, stopStateSync } = createDashboardAppState({ + stateDefaults, + osdUrlStateStorage: services.osdUrlStateStorage, + services, + instance, + }); - const { filterManager, queryString } = services.data.query; + 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)); + // 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()); + // 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), + })) + ), }, - 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, - } - ); + { + filters: opensearchFilters.FilterStateStore.APP_STATE, + query: true, + } + ); - setAppState(stateContainer); + setAppState(stateContainer); - return () => { - stopStateSync(); - stopSyncingAppFilters(); - }; + return () => { + stopStateSync(); + stopSyncingAppFilters(); + }; + } }, [eventEmitter, instance, services]); return { appState }; diff --git a/src/plugins/dashboard/public/application/utils/use/use_dashboard_container.tsx b/src/plugins/dashboard/public/application/utils/use/use_dashboard_container.tsx new file mode 100644 index 000000000000..f8a5136c494b --- /dev/null +++ b/src/plugins/dashboard/public/application/utils/use/use_dashboard_container.tsx @@ -0,0 +1,326 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useState } from 'react'; +import { EMPTY, Subscription, merge } from 'rxjs'; +import { catchError, distinctUntilChanged, map, mapTo, startWith, switchMap } from 'rxjs/operators'; +import deepEqual from 'fast-deep-equal'; +import { EventEmitter } from 'stream'; +import { useEffect } from 'react'; +import { opensearchFilters } from '../../../../../data/public'; +import { + DASHBOARD_CONTAINER_TYPE, + DashboardContainer, + DashboardContainerInput, + DashboardPanelState, +} from '../../embeddable'; +import { + ContainerOutput, + ErrorEmbeddable, + ViewMode, + isErrorEmbeddable, +} from '../../../embeddable_plugin'; +import { + convertPanelStateToSavedDashboardPanel, + convertSavedDashboardPanelToPanelState, +} from '../../lib/embeddable_saved_object_converters'; +import { DashboardEmptyScreen, DashboardEmptyScreenProps } from '../../dashboard_empty_screen'; +import { DashboardAppStateContainer, DashboardServices, SavedDashboardPanel } from '../../../types'; +import { migrateLegacyQuery } from '../../lib/migrate_legacy_query'; + +export const useDashboardContainer = ( + services: DashboardServices, + isChromeVisible: boolean, + eventEmitter: EventEmitter, + dashboardDom: HTMLElement | null, + savedDashboardInstance?: any, + appState?: DashboardAppStateContainer +) => { + const [dashboardContainer, setDashboardContainer] = useState(); + + useEffect(() => { + const getDashboardContainer = async () => { + try { + if (savedDashboardInstance && appState) { + let dashboardContainerEmbeddable: DashboardContainer | undefined; + try { + dashboardContainerEmbeddable = await createDashboardEmbeddable( + savedDashboardInstance, + services, + appState + ); + } catch (error) { + console.log(error); + } + setDashboardContainer(dashboardContainerEmbeddable); + } + } catch (error) { + console.log(error); + } + }; + + getDashboardContainer(); + }, [appState, dashboardDom, eventEmitter, isChromeVisible, savedDashboardInstance, services]); + + return { dashboardContainer }; +}; + +const createDashboardEmbeddable = async ( + savedDash: any, + dashboardServices: DashboardServices, + appState: DashboardAppStateContainer +) => { + let dashboardContainer: DashboardContainer; + let inputSubscription: Subscription | undefined; + let outputSubscription: Subscription | undefined; + + const { + embeddable, + data, + uiSettings, + http, + dashboardConfig, + embeddableCapabilities, + } = dashboardServices; + const { query: queryService } = data; + const filterManager = queryService.filterManager; + const timefilter = queryService.timefilter.timefilter; + const queryStringManager = queryService.queryString; + const { visualizeCapabilities, mapsCapabilities } = embeddableCapabilities; + // const dashboardDom = document.getElementById('dashboardViewport'); + const dashboardFactory = embeddable.getEmbeddableFactory< + DashboardContainerInput, + ContainerOutput, + DashboardContainer + >(DASHBOARD_CONTAINER_TYPE); + + const getShouldShowEditHelp = () => { + return ( + !savedDash.panels.length && + savedDash.viewMode === ViewMode.EDIT && + !dashboardConfig.getHideWriteControls() + ); + }; + + const getShouldShowViewHelp = () => { + return ( + !savedDash.panels.length && + savedDash.viewMode === ViewMode.VIEW && + !dashboardConfig.getHideWriteControls() + ); + }; + + const shouldShowUnauthorizedEmptyState = () => { + const readonlyMode = + !savedDash.panels.length && + !getShouldShowEditHelp() && + !getShouldShowViewHelp() && + dashboardConfig.getHideWriteControls(); + const userHasNoPermissions = + !savedDash.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 appStateData = appState.getState(); + const embeddablesMap: { + [key: string]: DashboardPanelState; + } = {}; + appStateData.panels.forEach((panel: SavedDashboardPanel) => { + embeddablesMap[panel.panelIndex] = convertSavedDashboardPanelToPanelState(panel); + }); + + const lastReloadRequestTime = 0; + return { + id: savedDash.id || '', + filters: data.query.filterManager.getFilters(), + hidePanelTitles: appStateData.options.hidePanelTitles, + query: savedDash.query, + timeRange: data.query.timefilter.timefilter.getTime(), + refreshConfig: data.query.timefilter.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) { + return 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; + }; + + // TODO: handle dashboard container input and output subsciptions + // issue: + 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((foo) => { + // This has to be first because handleDashboardContainerChanges causes + // appState.save which will cause refreshDashboardContainer to be called. + + if ( + !opensearchFilters.compareFilters( + container.getInput().filters, + filterManager.getFilters(), + opensearchFilters.COMPARE_ALL_OPTIONS + ) + ) { + // Add filters modifies the object passed to it, hence the clone deep. + filterManager.addFilters(_.cloneDeep(container.getInput().filters)); + + /* dashboardStateManager.applyFilters( + $scope.model.query, + container.getInput().filters + );*/ + + appState.transitions.set('query', queryStringManager.getQuery()); + } + // triggered when dashboard embeddable container has changes, and update the appState + // handleDashboardContainerChanges(container, appState, dashboardServices); + }); + return dashboardContainer; + } + }); + } + return undefined; +}; + +const handleDashboardContainerChanges = ( + dashboardContainer: DashboardContainer, + appState: DashboardAppStateContainer, + dashboardServices: DashboardServices +) => { + let dirty = false; + let dirtyBecauseOfInitialStateMigration = false; + + const appStateData = appState.getState(); + const savedDashboardPanelMap: { [key: string]: SavedDashboardPanel } = {}; + const { opensearchDashboardsVersion } = dashboardServices; + + const input = dashboardContainer.getInput(); + appStateData.panels.forEach((savedDashboardPanel) => { + if (input.panels[savedDashboardPanel.panelIndex] !== undefined) { + savedDashboardPanelMap[savedDashboardPanel.panelIndex] = savedDashboardPanel; + } else { + // A panel was deleted. + dirty = true; + } + }); + + const convertedPanelStateMap: { [key: string]: SavedDashboardPanel } = {}; + + Object.values(input.panels).forEach((panelState) => { + if (savedDashboardPanelMap[panelState.explicitInput.id] === undefined) { + dirty = true; + } + + convertedPanelStateMap[panelState.explicitInput.id] = convertPanelStateToSavedDashboardPanel( + panelState, + opensearchDashboardsVersion + ); + + if ( + !_.isEqual( + convertedPanelStateMap[panelState.explicitInput.id], + savedDashboardPanelMap[panelState.explicitInput.id] + ) + ) { + // A panel was changed + dirty = true; + + const oldVersion = savedDashboardPanelMap[panelState.explicitInput.id]?.version; + const newVersion = convertedPanelStateMap[panelState.explicitInput.id]?.version; + if (oldVersion && newVersion && oldVersion !== newVersion) { + dirtyBecauseOfInitialStateMigration = true; + } + } + }); + + if (dirty) { + appState.transitions.set('panels', Object.values(convertedPanelStateMap)); + if (dirtyBecauseOfInitialStateMigration) { + // this.saveState({ replace: true }); + } + } + + if (input.isFullScreenMode !== appStateData.fullScreenMode) { + appState.transitions.set('fullScreenMode', input.isFullScreenMode); + } + + if (input.expandedPanelId !== appStateData.expandedPanelId) { + appState.transitions.set('expandedPanelId', input.expandedPanelId); + } + + if (!_.isEqual(input.query, migrateLegacyQuery(appState.get().query))) { + appState.transitions.set('query', input.query); + } +}; diff --git a/src/plugins/dashboard/public/application/utils/use/use_editor_updates.ts b/src/plugins/dashboard/public/application/utils/use/use_editor_updates.ts new file mode 100644 index 000000000000..60dfb9ba927a --- /dev/null +++ b/src/plugins/dashboard/public/application/utils/use/use_editor_updates.ts @@ -0,0 +1,66 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import EventEmitter from 'events'; +import { useEffect, useState } from 'react'; +import { DashboardAppState, DashboardAppStateContainer, DashboardServices } from '../../../types'; +import { DashboardContainer } from '../../embeddable'; + +export const useEditorUpdates = ( + services: DashboardServices, + eventEmitter: EventEmitter, + dashboardInstance?: any, + dashboardContainer?: DashboardContainer, + appState?: DashboardAppStateContainer +) => { + const [isEmbeddableRendered, setIsEmbeddableRendered] = useState(false); + const [currentAppState, setCurrentAppState] = useState(); + const dom = document.getElementById('dashboardViewport'); + + const { + timefilter: { timefilter }, + filterManager, + queryString, + state$, + } = services.data.query; + + useEffect(() => { + if (appState && dashboardInstance && dashboardContainer) { + const initialState = appState.getState(); + setCurrentAppState(initialState); + + const unsubscribeStateUpdates = appState.subscribe((state) => { + setCurrentAppState(state); + dashboardContainer.reload(); + }); + + return () => { + unsubscribeStateUpdates(); + }; + } + }, [ + appState, + eventEmitter, + dashboardInstance, + services, + dashboardContainer, + isEmbeddableRendered, + currentAppState, + ]); + + useEffect(() => { + if (!dom || !dashboardContainer) { + return; + } + dashboardContainer.render(dom); + setIsEmbeddableRendered(true); + + return () => { + setIsEmbeddableRendered(false); + }; + }, [appState, dashboardInstance, currentAppState, dashboardContainer, state$, dom]); + + return { isEmbeddableRendered, currentAppState }; +}; 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 index e7e1633ac41b..a674a5e3e2a0 100644 --- 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 @@ -12,6 +12,7 @@ import { } from '../../../../../opensearch_dashboards_utils/public'; import { DashboardConstants } from '../../../dashboard_constants'; import { DashboardServices } from '../../../types'; +import { getDashboardInstance } from '../get_dashboard_instance'; /** * This effect is responsible for instantiating a saved dashboard or creating a new one @@ -23,22 +24,23 @@ export const useSavedDashboardInstance = ( isChromeVisible: boolean | undefined, dashboardIdFromUrl: string | undefined ) => { - const [state, setState] = useState<{ - savedDashboardInstance?: any; - }>({}); + const [savedDashboardInstance, setSavedDashboardInstance] = useState(); + const dashEditorRef = useRef(null); const dashboardId = useRef(''); + const dashboardDom = document.getElementById('dashboardViewport'); useEffect(() => { + const { + application: { navigateToApp }, + chrome, + history, + http: { basePath }, + notifications, + savedDashboards, + } = services; + const getSavedDashboardInstance = async () => { - const { - application: { navigateToApp }, - chrome, - history, - http: { basePath }, - notifications, - savedDashboards, - } = services; try { console.log('trying to get saved dashboard'); let savedDashboardInstance: any; @@ -91,7 +93,7 @@ export const useSavedDashboardInstance = ( } } - setState({ savedDashboardInstance }); + setSavedDashboardInstance(savedDashboardInstance); } catch (error) {} }; @@ -106,15 +108,13 @@ export const useSavedDashboardInstance = ( } else if ( dashboardIdFromUrl && dashboardId.current !== dashboardIdFromUrl && - state.savedDashboardInstance?.id !== dashboardIdFromUrl + savedDashboardInstance?.id !== dashboardIdFromUrl ) { dashboardId.current = dashboardIdFromUrl; - setState({}); + setSavedDashboardInstance({}); getSavedDashboardInstance(); } - }, [eventEmitter, isChromeVisible, services, state.savedDashboardInstance, dashboardIdFromUrl]); + }, [eventEmitter, isChromeVisible, services, savedDashboardInstance, dashboardIdFromUrl]); - return { - ...state, - }; + return savedDashboardInstance; }; diff --git a/src/plugins/dashboard/public/types.ts b/src/plugins/dashboard/public/types.ts index ddf2d7bf40d1..06b1f3d38bf0 100644 --- a/src/plugins/dashboard/public/types.ts +++ b/src/plugins/dashboard/public/types.ts @@ -273,3 +273,8 @@ export interface DashboardServices extends CoreStart { restorePreviousUrl: () => void; addBasePath?: (url: string) => string; } + +export interface DashEditorController { + render(props: any): void; + destroy(): void; +} diff --git a/src/plugins/visualize/public/application/utils/use/use_visualize_app_state.tsx b/src/plugins/visualize/public/application/utils/use/use_visualize_app_state.tsx index 91971a363c05..ee9fb6fb8093 100644 --- a/src/plugins/visualize/public/application/utils/use/use_visualize_app_state.tsx +++ b/src/plugins/visualize/public/application/utils/use/use_visualize_app_state.tsx @@ -68,6 +68,7 @@ export const useVisualizeAppState = ( }); const onDirtyStateChange = ({ isDirty }: { isDirty: boolean }) => { + console.log('on dirty state change', instance); if (!isDirty) { // it is important to update vis state with fresh data stateContainer.transitions.updateVisState(visStateToEditorState(instance, services).vis);