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 & {