diff --git a/src/plugins/dashboard/public/application/components/dashboard_editor.tsx b/src/plugins/dashboard/public/application/components/dashboard_editor.tsx index d77f73855c0a..fd4634abda2d 100644 --- a/src/plugins/dashboard/public/application/components/dashboard_editor.tsx +++ b/src/plugins/dashboard/public/application/components/dashboard_editor.tsx @@ -34,6 +34,7 @@ export const DashboardEditor = () => { services, isChromeVisible, eventEmitter, + dashboard, savedDashboardInstance, appState ); @@ -41,6 +42,7 @@ export const DashboardEditor = () => { const { isEmbeddableRendered, currentAppState } = useEditorUpdates( services, eventEmitter, + dashboard, savedDashboardInstance, dashboardContainer, appState @@ -59,21 +61,27 @@ export const DashboardEditor = () => { console.log('appStateData', appState?.getState()); console.log('currentAppState', currentAppState); console.log('isEmbeddableRendered', isEmbeddableRendered); + console.log('app state isDirty', appState?.getState().isDirty); console.log('dashboardContainer', dashboardContainer); return (
- {savedDashboardInstance && appState && dashboardContainer && currentAppState && ( - - )} + {savedDashboardInstance && + appState && + dashboardContainer && + currentAppState && + dashboard && ( + + )}
); 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 01fb0c9b91f8..329c3182ec02 100644 --- a/src/plugins/dashboard/public/application/components/dashboard_top_nav.tsx +++ b/src/plugins/dashboard/public/application/components/dashboard_top_nav.tsx @@ -14,11 +14,13 @@ import { DashboardAppStateContainer, DashboardAppState, DashboardServices } from import { getNavActions } from '../utils/get_nav_actions'; import { DashboardContainer } from '../embeddable'; import { isErrorEmbeddable } from '../../embeddable_plugin'; +import { Dashboard } from '../../dashboard'; interface DashboardTopNavProps { isChromeVisible: boolean; savedDashboardInstance: any; stateContainer: DashboardAppStateContainer; + dashboard: Dashboard; currentAppState: DashboardAppState; isEmbeddableRendered: boolean; dashboardContainer?: DashboardContainer; @@ -36,6 +38,7 @@ const TopNav = ({ isChromeVisible, savedDashboardInstance, stateContainer, + dashboard, currentAppState, isEmbeddableRendered, dashboardContainer, @@ -83,6 +86,7 @@ const TopNav = ({ stateContainer, savedDashboardInstance, services, + dashboard, dashboardContainer ); setTopNavMenu( @@ -101,6 +105,7 @@ const TopNav = ({ savedDashboardInstance, stateContainer, isEmbeddableRendered, + dashboard ]); useEffect(() => { diff --git a/src/plugins/dashboard/public/application/lib/get_app_state_defaults.ts b/src/plugins/dashboard/public/application/lib/get_app_state_defaults.ts index 95e5d70be438..aae4287870ea 100644 --- a/src/plugins/dashboard/public/application/lib/get_app_state_defaults.ts +++ b/src/plugins/dashboard/public/application/lib/get_app_state_defaults.ts @@ -46,5 +46,6 @@ export function getAppStateDefaults( query: savedDashboard.getQuery(), filters: savedDashboard.getFilters(), viewMode: savedDashboard.id || hideWriteControls ? ViewMode.VIEW : ViewMode.EDIT, + isDirty: false, }; } 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 index e4c0d9448a2d..8b7e248cc94f 100644 --- a/src/plugins/dashboard/public/application/utils/create_dashboard_app_state.tsx +++ b/src/plugins/dashboard/public/application/utils/create_dashboard_app_state.tsx @@ -53,6 +53,7 @@ export const createDashboardAppState = ({ [option]: value, }, }), + // setDashboard: (state) } as DashboardAppStateTransitions; /* make sure url ('_a') matches initial state diff --git a/src/plugins/dashboard/public/application/utils/get_nav_actions.tsx b/src/plugins/dashboard/public/application/utils/get_nav_actions.tsx index a36dcad0d785..6e2ad4dc17ec 100644 --- a/src/plugins/dashboard/public/application/utils/get_nav_actions.tsx +++ b/src/plugins/dashboard/public/application/utils/get_nav_actions.tsx @@ -30,6 +30,7 @@ import { DashboardContainer } from '../embeddable/dashboard_container'; import { DashboardConstants, createDashboardEditUrl } from '../../dashboard_constants'; import { unhashUrl } from '../../../../opensearch_dashboards_utils/public'; import { UrlParams } from '../components/dashboard_top_nav'; +import { Dashboard } from '../../dashboard'; interface UrlParamsSelectedMap { [UrlParams.SHOW_TOP_MENU]: boolean; @@ -46,6 +47,7 @@ export const getNavActions = ( stateContainer: DashboardAppStateContainer, savedDashboard: any, services: DashboardServices, + dashboard: Dashboard, dashboardContainer?: DashboardContainer ) => { const { @@ -292,40 +294,50 @@ export const getNavActions = ( function onChangeViewMode(newMode: ViewMode) { const isPageRefresh = newMode === appState.viewMode; const isLeavingEditMode = !isPageRefresh && newMode === ViewMode.VIEW; - // TODO: check if any query and filter changed - const willLoseChanges = isLeavingEditMode; + const willLoseChanges = isLeavingEditMode && stateContainer.getState().isDirty === true; + // If there are no changes, do not show the discard window if (!willLoseChanges) { stateContainer.transitions.set('viewMode', newMode); return; } + // If there are changes, show the discard window, and reset the states to original function revertChangesAndExitEditMode() { - stateContainer.transitions.set('viewMode', ViewMode.VIEW); const pathname = savedDashboard.id ? createDashboardEditUrl(savedDashboard.id) : DashboardConstants.CREATE_NEW_DASHBOARD_URL; history.push(pathname); - /* dashboardStateManager.resetState(); - // This is only necessary for new dashboards, which will default to Edit mode. - updateViewMode(ViewMode.VIEW); - - // We need to do a hard reset of the timepicker. appState will not reload like - // it does on 'open' because it's been saved to the url and the getAppState.previouslyStored() check on - // reload will cause it not to sync. - if (dashboardStateManager.getIsTimeSavedWithDashboard()) { - dashboardStateManager.syncTimefilterWithDashboardTime(timefilter); - dashboardStateManager.syncTimefilterWithDashboardRefreshInterval(timefilter); - } - - // Angular's $location skips this update because of history updates from syncState which happen simultaneously - // when calling osdUrl.change() angular schedules url update and when angular finally starts to process it, - // the update is considered outdated and angular skips it - // so have to use implementation of dashboardStateManager.changeDashboardUrl, which workarounds those issues - dashboardStateManager.changeDashboardUrl( - dash.id ? createDashboardEditUrl(dash.id) : DashboardConstants.CREATE_NEW_DASHBOARD_URL - );*/ + // This is only necessary for new dashboards, which will default to Edit mode. + stateContainer.transitions.set('viewMode', ViewMode.VIEW); + + // We need to reset the app state to its original state + if (dashboard.panels) { + stateContainer.transitions.set('panels', dashboard.panels); + } + + stateContainer.transitions.set('filters', dashboard.filters); + stateContainer.transitions.set('query', dashboard.query); + stateContainer.transitions.setOption('hidePanelTitles', dashboard.options.hidePanelTitles); + stateContainer.transitions.setOption('useMargins', dashboard.options.useMargins); + + // Need to see if needed + stateContainer.transitions.set('timeRestore', dashboard.timeRestore); + + // Since time filters are not tracked by app state, we need to manually reset it + if (stateContainer.getState().timeRestore) { + queryService.timefilter.timefilter.setTime({ + from: dashboard.timeFrom, + to: dashboard.timeTo, + }); + if (dashboard.refreshInterval) { + queryService.timefilter.timefilter.setRefreshInterval(dashboard.refreshInterval); + } + } + + // Set the isDirty flag back to false since we discard all the changes + stateContainer.transitions.set('isDirty', false); } overlays 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 6c0a3cddd53f..ecec53ef2152 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 @@ -15,7 +15,6 @@ import { DashboardAppStateContainer } from '../../../types'; import { migrateAppState, getAppStateDefaults } from '../../lib'; import { createDashboardAppState } from '../create_dashboard_app_state'; import { SavedObjectDashboard } from '../../../saved_dashboards'; -import { Dashboard, DashboardParams } from '../../../dashboard'; /** * This effect is responsible for instantiating the dashboard app state container, 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 index 71664fb4f9f4..b456a064c75a 100644 --- a/src/plugins/dashboard/public/application/utils/use/use_dashboard_container.tsx +++ b/src/plugins/dashboard/public/application/utils/use/use_dashboard_container.tsx @@ -19,6 +19,7 @@ import deepEqual from 'fast-deep-equal'; import { EventEmitter } from 'stream'; import { useEffect } from 'react'; import { i18n } from '@osd/i18n'; +import _ from 'lodash'; import { IndexPattern, opensearchFilters } from '../../../../../data/public'; import { DASHBOARD_CONTAINER_TYPE, @@ -50,11 +51,13 @@ import { migrateLegacyQuery } from '../../lib/migrate_legacy_query'; import { getSavedObjectFinder } from '../../../../../saved_objects/public'; import { DashboardConstants } from '../../../dashboard_constants'; import { SavedObjectDashboard } from '../../../saved_dashboards'; +import { Dashboard } from '../../../dashboard'; export const useDashboardContainer = ( services: DashboardServices, isChromeVisible: boolean, eventEmitter: EventEmitter, + dashboard?: Dashboard, savedDashboardInstance?: SavedObjectDashboard, appState?: DashboardAppStateContainer ) => { @@ -63,11 +66,12 @@ export const useDashboardContainer = ( useEffect(() => { const getDashboardContainer = async () => { try { - if (savedDashboardInstance && appState) { + if (savedDashboardInstance && appState && dashboard) { const dashboardContainerEmbeddable = await createDashboardEmbeddable( savedDashboardInstance, services, - appState + appState, + dashboard ); setDashboardContainer(dashboardContainerEmbeddable); @@ -83,7 +87,7 @@ export const useDashboardContainer = ( }; getDashboardContainer(); - }, [savedDashboardInstance, appState, services]); + }, [savedDashboardInstance, appState, services, dashboard]); useEffect(() => { const incomingEmbeddable = services.embeddable @@ -107,7 +111,8 @@ export const useDashboardContainer = ( const createDashboardEmbeddable = ( savedDash: any, dashboardServices: DashboardServices, - appState: DashboardAppStateContainer + appState: DashboardAppStateContainer, + dashboard: Dashboard ) => { let dashboardContainer: DashboardContainer; let inputSubscription: Subscription | undefined; @@ -343,7 +348,7 @@ const createDashboardEmbeddable = ( appState.transitions.set('query', queryStringManager.getQuery()); } // triggered when dashboard embeddable container has changes, and update the appState - handleDashboardContainerChanges(container, appState, dashboardServices); + handleDashboardContainerChanges(container, appState, dashboardServices, dashboard); }); return dashboardContainer; } @@ -355,7 +360,8 @@ const createDashboardEmbeddable = ( const handleDashboardContainerChanges = ( dashboardContainer: DashboardContainer, appState: DashboardAppStateContainer, - dashboardServices: DashboardServices + dashboardServices: DashboardServices, + dashboard: Dashboard ) => { let dirty = false; let dirtyBecauseOfInitialStateMigration = false; @@ -371,6 +377,7 @@ const handleDashboardContainerChanges = ( dirty = true; } }); + const convertedPanelStateMap: { [key: string]: SavedDashboardPanel } = {}; Object.values(input.panels).forEach((panelState) => { if (savedDashboardPanelMap[panelState.explicitInput.id] === undefined) { @@ -397,8 +404,8 @@ const handleDashboardContainerChanges = ( }); if (dirty) { appState.transitions.set('panels', Object.values(convertedPanelStateMap)); - if (dirtyBecauseOfInitialStateMigration) { - // this.saveState({ replace: true }); + if (!dirtyBecauseOfInitialStateMigration) { + appState.transitions.set('isDirty', true); } } if (input.isFullScreenMode !== appStateData.fullScreenMode) { 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 index 2a2a116d97b4..b9a1d1ef75ce 100644 --- a/src/plugins/dashboard/public/application/utils/use/use_editor_updates.ts +++ b/src/plugins/dashboard/public/application/utils/use/use_editor_updates.ts @@ -8,15 +8,21 @@ import { useEffect, useState } from 'react'; import { merge } from 'rxjs'; import { DashboardAppState, DashboardAppStateContainer, DashboardServices } from '../../../types'; import { DashboardContainer } from '../../embeddable'; +import { Dashboard } from '../../../dashboard'; export const useEditorUpdates = ( services: DashboardServices, eventEmitter: EventEmitter, + dashboard?: Dashboard, dashboardInstance?: any, dashboardContainer?: DashboardContainer, appState?: DashboardAppStateContainer ) => { const [isEmbeddableRendered, setIsEmbeddableRendered] = useState(false); + // We only mark dirty when there is changes in the panels, query, and filters + // We do not mark dirty for embed mode, view mode, full screen and etc + // The specific behaviors need to check the functional tests and previous dashboard + // const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); const [currentAppState, setCurrentAppState] = useState(); const dashboardDom = document.getElementById('dashboardViewport'); @@ -25,7 +31,7 @@ export const useEditorUpdates = ( } = services.data.query; useEffect(() => { - if (appState && dashboardInstance && dashboardContainer) { + if (appState && dashboardInstance && dashboardContainer && dashboard) { const initialState = appState.getState(); setCurrentAppState(initialState); @@ -36,11 +42,17 @@ export const useEditorUpdates = ( ); if (changes) { dashboardContainer.updateInput(changes); + + if (changes.filters || changes.query || changes.timeRange || changes.refreshConfig) { + appState.transitions.set('isDirty', true); + } } } }; const unsubscribeStateUpdates = appState.subscribe((state) => { + // If app state is changes, then set unsaved changes to true + // the only thing app state is not tracking is the time filter, need to check the previous dashboard if they count time filter change or not setCurrentAppState(state); refreshDashboardContainer(); }); @@ -69,6 +81,7 @@ export const useEditorUpdates = ( dashboardContainer, isEmbeddableRendered, timefilter, + dashboard, ]); useEffect(() => { 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 5ad82517287c..24c85687b806 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 @@ -44,10 +44,10 @@ export const useSavedDashboardInstance = ( const getSavedDashboardInstance = async () => { try { - let savedDashboardInstanceWithClass: any; + let savedDashboardInstance: any; if (history.location.pathname === '/create') { try { - savedDashboardInstanceWithClass = await getDashboardInstance(services); + savedDashboardInstance = await getDashboardInstance(services); } catch { redirectWhenMissing({ history, @@ -61,11 +61,11 @@ export const useSavedDashboardInstance = ( } } else if (dashboardIdFromUrl) { try { - savedDashboardInstanceWithClass = await getDashboardInstance( + savedDashboardInstance = await getDashboardInstance( services, dashboardIdFromUrl ); - const { savedDashboard } = savedDashboardInstanceWithClass; + const { savedDashboard } = savedDashboardInstance; // Update time filter to match the saved dashboard if time restore has been set to true when saving the dashboard // We should only set the time filter according to time restore once when we are loading the dashboard if (savedDashboard.timeRestore) { diff --git a/src/plugins/dashboard/public/dashboard.ts b/src/plugins/dashboard/public/dashboard.ts index 2c3b1dac79f1..4dc192c4d875 100644 --- a/src/plugins/dashboard/public/dashboard.ts +++ b/src/plugins/dashboard/public/dashboard.ts @@ -17,18 +17,19 @@ import { cloneDeep } from 'lodash'; import { Filter, ISearchSource, Query, RefreshInterval } from '../../data/public'; import { DashboardPanelState } from './application'; import { EmbeddableInput } from './embeddable_plugin'; +import { SavedDashboardPanel } from './types'; -export interface SerializedPanels { - [panelId: string]: DashboardPanelState; -} +// export interface SerializedPanels { +// [panelId: string]: DashboardPanelState; +// } export interface SerializedDashboard { id?: string; - timeRestore?: boolean; + timeRestore: boolean; timeTo?: string; timeFrom?: string; description?: string; - panels?: SerializedPanels; + panels: SavedDashboardPanel[]; options?: { hidePanelTitles: boolean; useMargins: boolean; @@ -37,8 +38,8 @@ export interface SerializedDashboard { lastSavedTitle: string; // TODO: DO WE STILL NEED THIS? refreshInterval?: RefreshInterval; // TODO: SHOULD THIS NOT BE OPTIONAL? searchSource?: ISearchSource; - query?: Query; - filters?: Filter[]; + query: Query; + filters: Filter[]; title?: string; } @@ -50,22 +51,27 @@ type PartialDashboardState = Partial; export class Dashboard { public id?: string; - public timeRestore?: boolean; + public timeRestore: boolean; public timeTo: string = ''; public timeFrom: string = ''; public description: string = ''; - public panels?: SerializedPanels; + public panels?: SavedDashboardPanel[]; public options: Record = {}; public uiState: string = ''; public refreshInterval?: RefreshInterval; public searchSource?: ISearchSource; - public query?: Query; - public filters?: Filter[]; + public query: Query; + public filters: Filter[]; public title?: string; // TODO: dashboardNew - pass version to dashboard class public version = '3.0.0'; + public isDirty = false; - constructor(dashboardState: SerializedDashboard = {} as any) {} + constructor(dashboardState: SerializedDashboard = {} as any) { + this.timeRestore = dashboardState.timeRestore; + this.query = cloneDeep(dashboardState.query); + this.filters = cloneDeep(dashboardState.filters); + } async setState(state: PartialDashboardState) { if (state.id) { @@ -84,7 +90,9 @@ export class Dashboard { this.description = state.description; } if (state.panels) { - this.panels = this.getPanels(state.panels); + // this panels is only JSON.parse() panels, we should convert them into the same type as app state panels + // app state store only JSON.parse() panels too + this.panels = cloneDeep(state.panels); } if (state.options) { this.options = state.options; @@ -101,29 +109,33 @@ export class Dashboard { if (state.searchSource) { this.searchSource = state.searchSource; } - if (state.query) { - this.query = this.getQuery(state.query); - } - if (state.filters) { - this.filters = this.getFilters(state.filters); - } + // if (state.query) { + // this.query = this.getQuery(state.query); + // } + // if (state.filters) { + // this.filters = this.getFilters(state.filters); + // } + } + + public setIsDirty(value: boolean) { + this.isDirty = value; } private getRefreshInterval(refreshInterval: RefreshInterval) { return cloneDeep(refreshInterval ?? {}); } - private getQuery(query: Query): Query { - return cloneDeep(query ?? ({} as Query)); - } + // private getQuery(query: Query): Query { + // return cloneDeep(query ?? ({} as Query)); + // } - private getFilters(filters: Filter[]) { - return cloneDeep(filters ?? ({} as Filter[])); - } + // private getFilters(filters: Filter[]) { + // return cloneDeep(filters ?? ({} as Filter[])); + // } - private getPanels(panels?: SerializedPanels) { - return cloneDeep(panels ?? ({} as SerializedPanels)); - } + // private getPanels(panels?: SerializedPanels) { + // return cloneDeep(panels ?? ({} as SerializedPanels)); + // } /* clone() { const serializedDashboard = this.serialize(); diff --git a/src/plugins/dashboard/public/types.ts b/src/plugins/dashboard/public/types.ts index efd571ca74fa..0e09216ea206 100644 --- a/src/plugins/dashboard/public/types.ts +++ b/src/plugins/dashboard/public/types.ts @@ -128,6 +128,7 @@ export interface DashboardAppState { viewMode: ViewMode; expandedPanelId?: string; savedQuery?: string; + isDirty: boolean; } export type DashboardAppStateDefaults = DashboardAppState & {