diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/share/show_share_modal.test.tsx b/src/plugins/dashboard/public/dashboard_app/top_nav/share/show_share_modal.test.tsx index 17ea8618ef57..0af39e925730 100644 --- a/src/plugins/dashboard/public/dashboard_app/top_nav/share/show_share_modal.test.tsx +++ b/src/plugins/dashboard/public/dashboard_app/top_nav/share/show_share_modal.test.tsx @@ -70,9 +70,7 @@ describe('ShowShareModal', () => { const getPropsAndShare = ( unsavedState?: Partial ): ShowShareModalProps => { - pluginServices.getServices().dashboardSessionStorage.getState = jest - .fn() - .mockReturnValue(unsavedState); + pluginServices.getServices().dashboardBackup.getState = jest.fn().mockReturnValue(unsavedState); return { isDirty: true, anchorElement: document.createElement('div'), diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/share/show_share_modal.tsx b/src/plugins/dashboard/public/dashboard_app/top_nav/share/show_share_modal.tsx index 98a899d6cac7..5147982d66e0 100644 --- a/src/plugins/dashboard/public/dashboard_app/top_nav/share/show_share_modal.tsx +++ b/src/plugins/dashboard/public/dashboard_app/top_nav/share/show_share_modal.tsx @@ -50,7 +50,7 @@ export function ShowShareModal({ }: ShowShareModalProps) { const { dashboardCapabilities: { createShortUrl: allowShortUrl }, - dashboardSessionStorage, + dashboardBackup, data: { query: { timefilter: { @@ -121,7 +121,7 @@ export function ShowShareModal({ }; let unsavedStateForLocator: DashboardAppLocatorParams = {}; - const unsavedDashboardState = dashboardSessionStorage.getState(savedObjectId); + const unsavedDashboardState = dashboardBackup.getState(savedObjectId); if (unsavedDashboardState) { unsavedStateForLocator = { diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/use_dashboard_menu_items.tsx b/src/plugins/dashboard/public/dashboard_app/top_nav/use_dashboard_menu_items.tsx index 1f05f7cc5428..643765bdfbab 100644 --- a/src/plugins/dashboard/public/dashboard_app/top_nav/use_dashboard_menu_items.tsx +++ b/src/plugins/dashboard/public/dashboard_app/top_nav/use_dashboard_menu_items.tsx @@ -38,6 +38,7 @@ export const useDashboardMenuItems = ({ */ const { share, + dashboardBackup, settings: { uiSettings }, dashboardCapabilities: { showWriteControls }, } = pluginServices.getServices(); @@ -127,18 +128,24 @@ export const useDashboardMenuItems = ({ const resetChanges = useCallback( (switchToViewMode: boolean = false) => { dashboard.clearOverlays(); - if (hasUnsavedChanges) { - confirmDiscardUnsavedChanges(() => { - batch(() => { - dashboard.resetToLastSavedState(); - if (switchToViewMode) dashboard.dispatch.setViewMode(ViewMode.VIEW); - }); - }, viewMode); - } else { - if (switchToViewMode) dashboard.dispatch.setViewMode(ViewMode.VIEW); + const switchModes = switchToViewMode + ? () => { + dashboard.dispatch.setViewMode(ViewMode.VIEW); + dashboardBackup.storeViewMode(ViewMode.VIEW); + } + : undefined; + if (!hasUnsavedChanges) { + switchModes?.(); + return; } + confirmDiscardUnsavedChanges(() => { + batch(() => { + dashboard.resetToLastSavedState(); + switchModes?.(); + }); + }, viewMode); }, - [dashboard, hasUnsavedChanges, viewMode] + [dashboard, dashboardBackup, hasUnsavedChanges, viewMode] ); /** @@ -170,6 +177,7 @@ export const useDashboardMenuItems = ({ testId: 'dashboardEditMode', className: 'eui-hideFor--s eui-hideFor--xs', // hide for small screens - editing doesn't work in mobile mode. run: () => { + dashboardBackup.storeViewMode(ViewMode.EDIT); dashboard.dispatch.setViewMode(ViewMode.EDIT); dashboard.clearOverlays(); }, @@ -231,18 +239,19 @@ export const useDashboardMenuItems = ({ } as TopNavMenuData, }; }, [ - disableTopNav, + quickSaveDashboard, isSaveInProgress, hasRunMigrations, hasUnsavedChanges, + dashboardBackup, + saveDashboardAs, + setIsLabsShown, + disableTopNav, + resetChanges, + isLabsShown, lastSavedId, showShare, dashboard, - setIsLabsShown, - isLabsShown, - quickSaveDashboard, - saveDashboardAs, - resetChanges, clone, ]); diff --git a/src/plugins/dashboard/public/dashboard_constants.ts b/src/plugins/dashboard/public/dashboard_constants.ts index 1764f55a176e..793923d203d0 100644 --- a/src/plugins/dashboard/public/dashboard_constants.ts +++ b/src/plugins/dashboard/public/dashboard_constants.ts @@ -77,7 +77,7 @@ export const DASHBOARD_CACHE_TTL = 1000 * 60 * 5; // time to live = 5 minutes // Default State // ------------------------------------------------------------------ export const DEFAULT_DASHBOARD_INPUT: Omit = { - viewMode: ViewMode.EDIT, // new dashboards start in edit mode. + viewMode: ViewMode.VIEW, timeRestore: false, query: { query: '', language: 'kuery' }, description: '', diff --git a/src/plugins/dashboard/public/dashboard_container/_dashboard_container_strings.ts b/src/plugins/dashboard/public/dashboard_container/_dashboard_container_strings.ts index 54e9989b4362..8bccc6ce4f5a 100644 --- a/src/plugins/dashboard/public/dashboard_container/_dashboard_container_strings.ts +++ b/src/plugins/dashboard/public/dashboard_container/_dashboard_container_strings.ts @@ -83,7 +83,12 @@ export const dashboardSavedObjectErrorStrings = { }), }; -export const panelStorageErrorStrings = { +export const backupServiceStrings = { + viewModeStorageError: (message: string) => + i18n.translate('dashboard.viewmodeBackup.error', { + defaultMessage: 'Error encountered while backing up view mode: {message}', + values: { message }, + }), getPanelsGetError: (message: string) => i18n.translate('dashboard.panelStorageError.getError', { defaultMessage: 'Error encountered while fetching unsaved changes: {message}', diff --git a/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.test.ts b/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.test.ts index c71e5cfc51d7..5a8a5931bed8 100644 --- a/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.test.ts +++ b/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.test.ts @@ -21,7 +21,7 @@ import { ControlGroupContainerFactory, } from '@kbn/controls-plugin/public'; import { Filter } from '@kbn/es-query'; -import { EmbeddablePackageState } from '@kbn/embeddable-plugin/public'; +import { EmbeddablePackageState, ViewMode } from '@kbn/embeddable-plugin/public'; import { createKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public'; import { createDashboard } from './create_dashboard'; @@ -109,7 +109,55 @@ test('passes managed state from the saved object into the Dashboard component st expect(dashboard!.getState().componentState.managed).toBe(true); }); -test('pulls state from session storage which overrides state from saved object', async () => { +test('pulls view mode from dashboard backup', async () => { + pluginServices.getServices().dashboardContentManagement.loadDashboardState = jest + .fn() + .mockResolvedValue({ + dashboardInput: DEFAULT_DASHBOARD_INPUT, + }); + pluginServices.getServices().dashboardBackup.getViewMode = jest + .fn() + .mockReturnValue(ViewMode.EDIT); + const dashboard = await createDashboard({ useSessionStorageIntegration: true }, 0, 'what-an-id'); + expect(dashboard).toBeDefined(); + expect(dashboard!.getState().explicitInput.viewMode).toBe(ViewMode.EDIT); +}); + +test('new dashboards start in edit mode', async () => { + pluginServices.getServices().dashboardBackup.getViewMode = jest + .fn() + .mockReturnValue(ViewMode.VIEW); + pluginServices.getServices().dashboardContentManagement.loadDashboardState = jest + .fn() + .mockResolvedValue({ + newDashboardCreated: true, + dashboardInput: { + ...DEFAULT_DASHBOARD_INPUT, + description: 'wow this description is okay', + }, + }); + const dashboard = await createDashboard({ useSessionStorageIntegration: true }, 0, 'wow-such-id'); + expect(dashboard).toBeDefined(); + expect(dashboard!.getState().explicitInput.viewMode).toBe(ViewMode.EDIT); +}); + +test('managed dashboards start in view mode', async () => { + pluginServices.getServices().dashboardBackup.getViewMode = jest + .fn() + .mockReturnValue(ViewMode.EDIT); + pluginServices.getServices().dashboardContentManagement.loadDashboardState = jest + .fn() + .mockResolvedValue({ + dashboardInput: DEFAULT_DASHBOARD_INPUT, + managed: true, + }); + const dashboard = await createDashboard({}, 0, 'what-an-id'); + expect(dashboard).toBeDefined(); + expect(dashboard!.getState().componentState.managed).toBe(true); + expect(dashboard!.getState().explicitInput.viewMode).toBe(ViewMode.VIEW); +}); + +test('pulls state from backup which overrides state from saved object', async () => { pluginServices.getServices().dashboardContentManagement.loadDashboardState = jest .fn() .mockResolvedValue({ @@ -118,7 +166,7 @@ test('pulls state from session storage which overrides state from saved object', description: 'wow this description is okay', }, }); - pluginServices.getServices().dashboardSessionStorage.getState = jest + pluginServices.getServices().dashboardBackup.getState = jest .fn() .mockReturnValue({ description: 'wow this description marginally better' }); const dashboard = await createDashboard({ useSessionStorageIntegration: true }, 0, 'wow-such-id'); @@ -137,7 +185,7 @@ test('pulls state from creation options initial input which overrides all other description: 'wow this description is okay', }, }); - pluginServices.getServices().dashboardSessionStorage.getState = jest + pluginServices.getServices().dashboardBackup.getState = jest .fn() .mockReturnValue({ description: 'wow this description marginally better' }); const dashboard = await createDashboard( diff --git a/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.ts b/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.ts index e843d07ad6ff..c505568da43c 100644 --- a/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.ts +++ b/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.ts @@ -135,8 +135,9 @@ export const initializeDashboard = async ({ controlGroup?: ControlGroupContainer; }) => { const { - dashboardSessionStorage, + dashboardBackup, embeddable: { getEmbeddableFactory }, + dashboardCapabilities: { showWriteControls }, data: { query: queryService, search: { session }, @@ -170,24 +171,43 @@ export const initializeDashboard = async ({ } // -------------------------------------------------------------------------------------- - // Gather input from session storage if integration is used. + // Gather input from session storage and local storage if integration is used. // -------------------------------------------------------------------------------------- const sessionStorageInput = ((): Partial | undefined => { if (!useSessionStorageIntegration) return; - return dashboardSessionStorage.getState(loadDashboardReturn.dashboardId); + return dashboardBackup.getState(loadDashboardReturn.dashboardId); })(); // -------------------------------------------------------------------------------------- // Combine input from saved object, session storage, & passed input to create initial input. // -------------------------------------------------------------------------------------- + const initialViewMode = (() => { + if (loadDashboardReturn.managed || !showWriteControls) return ViewMode.VIEW; + if ( + loadDashboardReturn.newDashboardCreated || + dashboardBackup.dashboardHasUnsavedEdits(loadDashboardReturn.dashboardId) + ) { + return ViewMode.EDIT; + } + + return dashboardBackup.getViewMode(); + })(); + const overrideInput = getInitialInput?.(); const initialInput: DashboardContainerInput = cloneDeep({ ...DEFAULT_DASHBOARD_INPUT, ...(loadDashboardReturn?.dashboardInput ?? {}), ...sessionStorageInput, + + ...(initialViewMode ? { viewMode: initialViewMode } : {}), ...overrideInput, }); + // Back up any view mode passed in explicitly. + if (overrideInput?.viewMode) { + dashboardBackup.storeViewMode(overrideInput?.viewMode); + } + initialInput.executionContext = { type: 'dashboard', description: initialInput.title, diff --git a/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx b/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx index 38c5c8b07797..4c88d246f6ca 100644 --- a/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx +++ b/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx @@ -402,13 +402,13 @@ export class DashboardContainer extends Container { this.dispatch.setLastSavedInput(loadDashboardReturn?.dashboardInput); this.dispatch.setManaged(loadDashboardReturn?.managed); this.dispatch.setAnimatePanelTransforms(false); // prevents panels from animating on navigate. this.dispatch.setLastSavedId(newSavedObjectId); }); + this.updateInput(newInput); dashboardContainerReady$.next(this); }; diff --git a/src/plugins/dashboard/public/dashboard_container/state/diffing/dashboard_diffing_integration.ts b/src/plugins/dashboard/public/dashboard_container/state/diffing/dashboard_diffing_integration.ts index 040acd2087df..13ff33b6bf9c 100644 --- a/src/plugins/dashboard/public/dashboard_container/state/diffing/dashboard_diffing_integration.ts +++ b/src/plugins/dashboard/public/dashboard_container/state/diffing/dashboard_diffing_integration.ts @@ -211,8 +211,8 @@ function backupUnsavedChanges( this: DashboardContainer, unsavedChanges: Partial ) { - const { dashboardSessionStorage } = pluginServices.getServices(); - dashboardSessionStorage.setState( + const { dashboardBackup } = pluginServices.getServices(); + dashboardBackup.setState( this.getDashboardSavedObjectId(), omit(unsavedChanges, keysToOmitFromSessionStorage) ); diff --git a/src/plugins/dashboard/public/dashboard_listing/dashboard_listing_empty_prompt.tsx b/src/plugins/dashboard/public/dashboard_listing/dashboard_listing_empty_prompt.tsx index d1460c53f23e..098dbcc0b493 100644 --- a/src/plugins/dashboard/public/dashboard_listing/dashboard_listing_empty_prompt.tsx +++ b/src/plugins/dashboard/public/dashboard_listing/dashboard_listing_empty_prompt.tsx @@ -24,7 +24,7 @@ import { } from './_dashboard_listing_strings'; import { pluginServices } from '../services/plugin_services'; import { confirmDiscardUnsavedChanges } from './confirm_overlays'; -import { DASHBOARD_PANELS_UNSAVED_ID } from '../services/dashboard_session_storage/dashboard_session_storage_service'; +import { DASHBOARD_PANELS_UNSAVED_ID } from '../services/dashboard_backup/dashboard_backup_service'; import { DashboardListingProps } from './types'; export interface DashboardListingEmptyPromptProps { @@ -46,7 +46,7 @@ export const DashboardListingEmptyPrompt = ({ }: DashboardListingEmptyPromptProps) => { const { application, - dashboardSessionStorage, + dashboardBackup, dashboardCapabilities: { showWriteControls }, } = pluginServices.getServices(); @@ -77,8 +77,8 @@ export const DashboardListingEmptyPrompt = ({ color="danger" onClick={() => confirmDiscardUnsavedChanges(() => { - dashboardSessionStorage.clearState(DASHBOARD_PANELS_UNSAVED_ID); - setUnsavedDashboardIds(dashboardSessionStorage.getDashboardIdsWithUnsavedChanges()); + dashboardBackup.clearState(DASHBOARD_PANELS_UNSAVED_ID); + setUnsavedDashboardIds(dashboardBackup.getDashboardIdsWithUnsavedChanges()); }) } data-test-subj="discardDashboardPromptButton" @@ -105,7 +105,7 @@ export const DashboardListingEmptyPrompt = ({ isEditingFirstDashboard, createItem, disableCreateDashboardButton, - dashboardSessionStorage, + dashboardBackup, setUnsavedDashboardIds, goToDashboard, ]); diff --git a/src/plugins/dashboard/public/dashboard_listing/dashboard_unsaved_listing.test.tsx b/src/plugins/dashboard/public/dashboard_listing/dashboard_unsaved_listing.test.tsx index edaaa21d9e08..81aa4bc07360 100644 --- a/src/plugins/dashboard/public/dashboard_listing/dashboard_unsaved_listing.test.tsx +++ b/src/plugins/dashboard/public/dashboard_listing/dashboard_unsaved_listing.test.tsx @@ -14,7 +14,7 @@ import { findTestSubject } from '@elastic/eui/lib/test'; import { pluginServices } from '../services/plugin_services'; import { DashboardUnsavedListing, DashboardUnsavedListingProps } from './dashboard_unsaved_listing'; -import { DASHBOARD_PANELS_UNSAVED_ID } from '../services/dashboard_session_storage/dashboard_session_storage_service'; +import { DASHBOARD_PANELS_UNSAVED_ID } from '../services/dashboard_backup/dashboard_backup_service'; import { ViewMode } from '@kbn/embeddable-plugin/public'; const makeDefaultProps = (): DashboardUnsavedListingProps => ({ @@ -91,7 +91,7 @@ describe('Unsaved listing', () => { waitFor(() => { component.update(); expect(pluginServices.getServices().overlays.openConfirm).toHaveBeenCalled(); - expect(pluginServices.getServices().dashboardSessionStorage.clearState).toHaveBeenCalledWith( + expect(pluginServices.getServices().dashboardBackup.clearState).toHaveBeenCalledWith( 'dashboardUnsavedOne' ); }); @@ -125,16 +125,16 @@ describe('Unsaved listing', () => { const { component } = mountWith({ props }); waitFor(() => { component.update(); - expect(pluginServices.getServices().dashboardSessionStorage.clearState).toHaveBeenCalledWith( + expect(pluginServices.getServices().dashboardBackup.clearState).toHaveBeenCalledWith( 'failCase1' ); - expect(pluginServices.getServices().dashboardSessionStorage.clearState).toHaveBeenCalledWith( + expect(pluginServices.getServices().dashboardBackup.clearState).toHaveBeenCalledWith( 'failCase2' ); // clearing panels from dashboard with errors should cause getDashboardIdsWithUnsavedChanges to be called again. expect( - pluginServices.getServices().dashboardSessionStorage.getDashboardIdsWithUnsavedChanges + pluginServices.getServices().dashboardBackup.getDashboardIdsWithUnsavedChanges ).toHaveBeenCalledTimes(2); }); }); diff --git a/src/plugins/dashboard/public/dashboard_listing/dashboard_unsaved_listing.tsx b/src/plugins/dashboard/public/dashboard_listing/dashboard_unsaved_listing.tsx index ee3f4c472bc1..72580f7546bb 100644 --- a/src/plugins/dashboard/public/dashboard_listing/dashboard_unsaved_listing.tsx +++ b/src/plugins/dashboard/public/dashboard_listing/dashboard_unsaved_listing.tsx @@ -23,7 +23,7 @@ import { pluginServices } from '../services/plugin_services'; import { confirmDiscardUnsavedChanges } from './confirm_overlays'; import { DashboardAttributes } from '../../common/content_management'; import { dashboardUnsavedListingStrings, getNewDashboardTitle } from './_dashboard_listing_strings'; -import { DASHBOARD_PANELS_UNSAVED_ID } from '../services/dashboard_session_storage/dashboard_session_storage_service'; +import { DASHBOARD_PANELS_UNSAVED_ID } from '../services/dashboard_backup/dashboard_backup_service'; const DashboardUnsavedItem = ({ id, @@ -116,7 +116,7 @@ export const DashboardUnsavedListing = ({ refreshUnsavedDashboards, }: DashboardUnsavedListingProps) => { const { - dashboardSessionStorage, + dashboardBackup, dashboardContentManagement: { findDashboards }, } = pluginServices.getServices(); @@ -132,11 +132,11 @@ export const DashboardUnsavedListing = ({ const onDiscard = useCallback( (id?: string) => { confirmDiscardUnsavedChanges(() => { - dashboardSessionStorage.clearState(id); + dashboardBackup.clearState(id); refreshUnsavedDashboards(); }); }, - [refreshUnsavedDashboards, dashboardSessionStorage] + [refreshUnsavedDashboards, dashboardBackup] ); useEffect(() => { @@ -156,7 +156,7 @@ export const DashboardUnsavedListing = ({ const newItems = results.reduce((map, result) => { if (result.status === 'error') { hasError = true; - dashboardSessionStorage.clearState(result.id); + dashboardBackup.clearState(result.id); return map; } return { @@ -173,7 +173,7 @@ export const DashboardUnsavedListing = ({ return () => { canceled = true; }; - }, [refreshUnsavedDashboards, dashboardSessionStorage, unsavedDashboardIds, findDashboards]); + }, [refreshUnsavedDashboards, dashboardBackup, unsavedDashboardIds, findDashboards]); return unsavedDashboardIds.length === 0 ? null : ( <> diff --git a/src/plugins/dashboard/public/dashboard_listing/hooks/use_dashboard_listing_table.test.tsx b/src/plugins/dashboard/public/dashboard_listing/hooks/use_dashboard_listing_table.test.tsx index d16e8b4b7e3f..f30ed1b21f1d 100644 --- a/src/plugins/dashboard/public/dashboard_listing/hooks/use_dashboard_listing_table.test.tsx +++ b/src/plugins/dashboard/public/dashboard_listing/hooks/use_dashboard_listing_table.test.tsx @@ -46,15 +46,13 @@ describe('useDashboardListingTable', () => { beforeEach(() => { jest.clearAllMocks(); - getPluginServices.dashboardSessionStorage.dashboardHasUnsavedEdits = jest - .fn() - .mockReturnValue(true); + getPluginServices.dashboardBackup.dashboardHasUnsavedEdits = jest.fn().mockReturnValue(true); - getPluginServices.dashboardSessionStorage.getDashboardIdsWithUnsavedChanges = jest + getPluginServices.dashboardBackup.getDashboardIdsWithUnsavedChanges = jest .fn() .mockReturnValue([]); - getPluginServices.dashboardSessionStorage.clearState = clearStateMock; + getPluginServices.dashboardBackup.clearState = clearStateMock; getPluginServices.dashboardCapabilities.showWriteControls = true; getPluginServices.dashboardContentManagement.deleteDashboards = deleteDashboards; getPluginServices.settings.uiSettings.get = getUiSettingsMock; diff --git a/src/plugins/dashboard/public/dashboard_listing/hooks/use_dashboard_listing_table.tsx b/src/plugins/dashboard/public/dashboard_listing/hooks/use_dashboard_listing_table.tsx index 8c933d0fb28b..80b39ff0e9d6 100644 --- a/src/plugins/dashboard/public/dashboard_listing/hooks/use_dashboard_listing_table.tsx +++ b/src/plugins/dashboard/public/dashboard_listing/hooks/use_dashboard_listing_table.tsx @@ -87,7 +87,7 @@ export const useDashboardListingTable = ({ showCreateDashboardButton?: boolean; }): UseDashboardListingTableReturnType => { const { - dashboardSessionStorage, + dashboardBackup, dashboardCapabilities: { showWriteControls }, settings: { uiSettings }, dashboardContentManagement: { @@ -106,30 +106,30 @@ export const useDashboardListingTable = ({ const [pageDataTestSubject, setPageDataTestSubject] = useState(); const [hasInitialFetchReturned, setHasInitialFetchReturned] = useState(false); const [unsavedDashboardIds, setUnsavedDashboardIds] = useState( - dashboardSessionStorage.getDashboardIdsWithUnsavedChanges() + dashboardBackup.getDashboardIdsWithUnsavedChanges() ); const listingLimit = uiSettings.get(SAVED_OBJECTS_LIMIT_SETTING); const initialPageSize = uiSettings.get(SAVED_OBJECTS_PER_PAGE_SETTING); const createItem = useCallback(() => { - if (useSessionStorageIntegration && dashboardSessionStorage.dashboardHasUnsavedEdits()) { + if (useSessionStorageIntegration && dashboardBackup.dashboardHasUnsavedEdits()) { confirmCreateWithUnsaved(() => { - dashboardSessionStorage.clearState(); + dashboardBackup.clearState(); goToDashboard(); }, goToDashboard); return; } goToDashboard(); - }, [dashboardSessionStorage, goToDashboard, useSessionStorageIntegration]); + }, [dashboardBackup, goToDashboard, useSessionStorageIntegration]); const updateItemMeta = useCallback( async (props: Pick) => { await updateDashboardMeta(props); - setUnsavedDashboardIds(dashboardSessionStorage.getDashboardIdsWithUnsavedChanges()); + setUnsavedDashboardIds(dashboardBackup.getDashboardIdsWithUnsavedChanges()); }, - [dashboardSessionStorage, updateDashboardMeta] + [dashboardBackup, updateDashboardMeta] ); const contentEditorValidators: OpenContentEditorParams['customValidators'] = useMemo( @@ -232,7 +232,7 @@ export const useDashboardListingTable = ({ await deleteDashboards( dashboardsToDelete.map(({ id }) => { - dashboardSessionStorage.clearState(id); + dashboardBackup.clearState(id); return id; }) ); @@ -252,9 +252,9 @@ export const useDashboardListingTable = ({ }); } - setUnsavedDashboardIds(dashboardSessionStorage.getDashboardIdsWithUnsavedChanges()); + setUnsavedDashboardIds(dashboardBackup.getDashboardIdsWithUnsavedChanges()); }, - [dashboardSessionStorage, deleteDashboards, toasts] + [dashboardBackup, deleteDashboards, toasts] ); const editItem = useCallback( @@ -324,8 +324,8 @@ export const useDashboardListingTable = ({ ); const refreshUnsavedDashboards = useCallback( - () => setUnsavedDashboardIds(dashboardSessionStorage.getDashboardIdsWithUnsavedChanges()), - [dashboardSessionStorage] + () => setUnsavedDashboardIds(dashboardBackup.getDashboardIdsWithUnsavedChanges()), + [dashboardBackup] ); return { diff --git a/src/plugins/dashboard/public/services/dashboard_session_storage/dashboard_session_storage.stub.ts b/src/plugins/dashboard/public/services/dashboard_backup/dashboard_backup.stub.ts similarity index 71% rename from src/plugins/dashboard/public/services/dashboard_session_storage/dashboard_session_storage.stub.ts rename to src/plugins/dashboard/public/services/dashboard_backup/dashboard_backup.stub.ts index 4ae1879122d2..e56df954afad 100644 --- a/src/plugins/dashboard/public/services/dashboard_session_storage/dashboard_session_storage.stub.ts +++ b/src/plugins/dashboard/public/services/dashboard_backup/dashboard_backup.stub.ts @@ -7,16 +7,17 @@ */ import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { DashboardSessionStorageServiceType } from './types'; +import { DashboardBackupServiceType } from './types'; -type DashboardSessionStorageServiceFactory = - PluginServiceFactory; +type DashboardBackupServiceFactory = PluginServiceFactory; -export const dashboardSessionStorageServiceFactory: DashboardSessionStorageServiceFactory = () => { +export const dashboardBackupServiceFactory: DashboardBackupServiceFactory = () => { return { clearState: jest.fn(), getState: jest.fn().mockReturnValue(undefined), setState: jest.fn(), + getViewMode: jest.fn(), + storeViewMode: jest.fn(), getDashboardIdsWithUnsavedChanges: jest .fn() .mockReturnValue(['dashboardUnsavedOne', 'dashboardUnsavedTwo']), diff --git a/src/plugins/dashboard/public/services/dashboard_session_storage/dashboard_session_storage_service.ts b/src/plugins/dashboard/public/services/dashboard_backup/dashboard_backup_service.ts similarity index 73% rename from src/plugins/dashboard/public/services/dashboard_session_storage/dashboard_session_storage_service.ts rename to src/plugins/dashboard/public/services/dashboard_backup/dashboard_backup_service.ts index 279d7fefb6d3..707ba9780ee9 100644 --- a/src/plugins/dashboard/public/services/dashboard_session_storage/dashboard_session_storage_service.ts +++ b/src/plugins/dashboard/public/services/dashboard_backup/dashboard_backup_service.ts @@ -15,34 +15,37 @@ import type { KibanaPluginServiceFactory } from '@kbn/presentation-util-plugin/p import { DashboardSpacesService } from '../spaces/types'; import type { DashboardStartDependencies } from '../../plugin'; -import type { DashboardSessionStorageServiceType } from './types'; +import type { DashboardBackupServiceType } from './types'; import type { DashboardContainerInput } from '../../../common'; import { DashboardNotificationsService } from '../notifications/types'; -import { panelStorageErrorStrings } from '../../dashboard_container/_dashboard_container_strings'; +import { backupServiceStrings } from '../../dashboard_container/_dashboard_container_strings'; export const DASHBOARD_PANELS_UNSAVED_ID = 'unsavedDashboard'; const DASHBOARD_PANELS_SESSION_KEY = 'dashboardStateManagerPanels'; +const DASHBOARD_VIEWMODE_LOCAL_KEY = 'dashboardViewMode'; -interface DashboardSessionStorageRequiredServices { +interface DashboardBackupRequiredServices { notifications: DashboardNotificationsService; spaces: DashboardSpacesService; } -export type DashboardSessionStorageServiceFactory = KibanaPluginServiceFactory< - DashboardSessionStorageServiceType, +export type DashboardBackupServiceFactory = KibanaPluginServiceFactory< + DashboardBackupServiceType, DashboardStartDependencies, - DashboardSessionStorageRequiredServices + DashboardBackupRequiredServices >; -class DashboardSessionStorageService implements DashboardSessionStorageServiceType { +class DashboardBackupService implements DashboardBackupServiceType { private activeSpaceId: string; private sessionStorage: Storage; + private localStorage: Storage; private notifications: DashboardNotificationsService; private spaces: DashboardSpacesService; - constructor(requiredServices: DashboardSessionStorageRequiredServices) { + constructor(requiredServices: DashboardBackupRequiredServices) { ({ notifications: this.notifications, spaces: this.spaces } = requiredServices); this.sessionStorage = new Storage(sessionStorage); + this.localStorage = new Storage(localStorage); this.activeSpaceId = 'default'; if (this.spaces.getActiveSpace$) { @@ -52,6 +55,21 @@ class DashboardSessionStorageService implements DashboardSessionStorageServiceTy } } + public getViewMode = (): ViewMode => { + return this.localStorage.get(DASHBOARD_VIEWMODE_LOCAL_KEY); + }; + + public storeViewMode = (viewMode: ViewMode) => { + try { + this.localStorage.set(DASHBOARD_VIEWMODE_LOCAL_KEY, viewMode); + } catch (e) { + this.notifications.toasts.addDanger({ + title: backupServiceStrings.viewModeStorageError(e.message), + 'data-test-subj': 'dashboardViewmodeBackupFailure', + }); + } + }; + public clearState(id = DASHBOARD_PANELS_UNSAVED_ID) { try { const sessionStorage = this.sessionStorage.get(DASHBOARD_PANELS_SESSION_KEY); @@ -62,7 +80,7 @@ class DashboardSessionStorageService implements DashboardSessionStorageServiceTy } } catch (e) { this.notifications.toasts.addDanger({ - title: panelStorageErrorStrings.getPanelsClearError(e.message), + title: backupServiceStrings.getPanelsClearError(e.message), 'data-test-subj': 'dashboardPanelsClearFailure', }); } @@ -73,7 +91,7 @@ class DashboardSessionStorageService implements DashboardSessionStorageServiceTy return this.sessionStorage.get(DASHBOARD_PANELS_SESSION_KEY)?.[this.activeSpaceId]?.[id]; } catch (e) { this.notifications.toasts.addDanger({ - title: panelStorageErrorStrings.getPanelsGetError(e.message), + title: backupServiceStrings.getPanelsGetError(e.message), 'data-test-subj': 'dashboardPanelsGetFailure', }); } @@ -86,7 +104,7 @@ class DashboardSessionStorageService implements DashboardSessionStorageServiceTy this.sessionStorage.set(DASHBOARD_PANELS_SESSION_KEY, sessionStateStorage); } catch (e) { this.notifications.toasts.addDanger({ - title: panelStorageErrorStrings.getPanelsSetError(e.message), + title: backupServiceStrings.getPanelsSetError(e.message), 'data-test-subj': 'dashboardPanelsSetFailure', }); } @@ -110,7 +128,7 @@ class DashboardSessionStorageService implements DashboardSessionStorageServiceTy return dashboardsWithUnsavedChanges; } catch (e) { this.notifications.toasts.addDanger({ - title: panelStorageErrorStrings.getPanelsGetError(e.message), + title: backupServiceStrings.getPanelsGetError(e.message), 'data-test-subj': 'dashboardPanelsGetFailure', }); return []; @@ -122,9 +140,9 @@ class DashboardSessionStorageService implements DashboardSessionStorageServiceTy } } -export const dashboardSessionStorageServiceFactory: DashboardSessionStorageServiceFactory = ( +export const dashboardBackupServiceFactory: DashboardBackupServiceFactory = ( core, requiredServices ) => { - return new DashboardSessionStorageService(requiredServices); + return new DashboardBackupService(requiredServices); }; diff --git a/src/plugins/dashboard/public/services/dashboard_session_storage/types.ts b/src/plugins/dashboard/public/services/dashboard_backup/types.ts similarity index 80% rename from src/plugins/dashboard/public/services/dashboard_session_storage/types.ts rename to src/plugins/dashboard/public/services/dashboard_backup/types.ts index e02d6463e2d3..c302295af832 100644 --- a/src/plugins/dashboard/public/services/dashboard_session_storage/types.ts +++ b/src/plugins/dashboard/public/services/dashboard_backup/types.ts @@ -6,12 +6,15 @@ * Side Public License, v 1. */ +import { ViewMode } from '@kbn/embeddable-plugin/public'; import type { DashboardContainerInput } from '../../../common'; -export interface DashboardSessionStorageServiceType { +export interface DashboardBackupServiceType { clearState: (id?: string) => void; getState: (id: string | undefined) => Partial | undefined; setState: (id: string | undefined, newState: Partial) => void; + getViewMode: () => ViewMode; + storeViewMode: (viewMode: ViewMode) => void; getDashboardIdsWithUnsavedChanges: () => string[]; dashboardHasUnsavedEdits: (id?: string) => boolean; } diff --git a/src/plugins/dashboard/public/services/dashboard_content_management/dashboard_content_management_service.ts b/src/plugins/dashboard/public/services/dashboard_content_management/dashboard_content_management_service.ts index b3689b9be423..0cac2dff75e3 100644 --- a/src/plugins/dashboard/public/services/dashboard_content_management/dashboard_content_management_service.ts +++ b/src/plugins/dashboard/public/services/dashboard_content_management/dashboard_content_management_service.ts @@ -43,9 +43,9 @@ export const dashboardContentManagementServiceFactory: DashboardContentManagemen data, embeddable, notifications, + dashboardBackup, initializerContext, savedObjectsTagging, - dashboardSessionStorage, } = requiredServices; return { loadDashboardState: ({ id }) => @@ -64,10 +64,10 @@ export const dashboardContentManagementServiceFactory: DashboardContentManagemen lastSavedId, currentState, notifications, + dashboardBackup, contentManagement, initializerContext, savedObjectsTagging, - dashboardSessionStorage, }), findDashboards: { search: ({ hasReference, hasNoReference, search, size, options }) => diff --git a/src/plugins/dashboard/public/services/dashboard_content_management/lib/load_dashboard_state.ts b/src/plugins/dashboard/public/services/dashboard_content_management/lib/load_dashboard_state.ts index 21ab8c8143d6..c022e05d91d3 100644 --- a/src/plugins/dashboard/public/services/dashboard_content_management/lib/load_dashboard_state.ts +++ b/src/plugins/dashboard/public/services/dashboard_content_management/lib/load_dashboard_state.ts @@ -56,7 +56,9 @@ export const loadDashboardState = async ({ /** * This is a newly created dashboard, so there is no saved object state to load. */ - if (!savedObjectId) return { dashboardInput: newDashboardState, dashboardFound: true }; + if (!savedObjectId) { + return { dashboardInput: newDashboardState, dashboardFound: true, newDashboardCreated: true }; + } /** * Load the saved object from Content Management diff --git a/src/plugins/dashboard/public/services/dashboard_content_management/lib/save_dashboard_state.ts b/src/plugins/dashboard/public/services/dashboard_content_management/lib/save_dashboard_state.ts index eac769f03de6..20d36688307d 100644 --- a/src/plugins/dashboard/public/services/dashboard_content_management/lib/save_dashboard_state.ts +++ b/src/plugins/dashboard/public/services/dashboard_content_management/lib/save_dashboard_state.ts @@ -63,9 +63,9 @@ type SaveDashboardStateProps = SaveDashboardProps & { contentManagement: DashboardStartDependencies['contentManagement']; embeddable: DashboardContentManagementRequiredServices['embeddable']; notifications: DashboardContentManagementRequiredServices['notifications']; + dashboardBackup: DashboardContentManagementRequiredServices['dashboardBackup']; initializerContext: DashboardContentManagementRequiredServices['initializerContext']; savedObjectsTagging: DashboardContentManagementRequiredServices['savedObjectsTagging']; - dashboardSessionStorage: DashboardContentManagementRequiredServices['dashboardSessionStorage']; }; export const saveDashboardState = async ({ @@ -74,9 +74,9 @@ export const saveDashboardState = async ({ lastSavedId, saveOptions, currentState, + dashboardBackup, contentManagement, savedObjectsTagging, - dashboardSessionStorage, notifications: { toasts }, }: SaveDashboardStateProps): Promise => { const { @@ -202,7 +202,7 @@ export const saveDashboardState = async ({ * If the dashboard id has been changed, redirect to the new ID to keep the url param in sync. */ if (newId !== lastSavedId) { - dashboardSessionStorage.clearState(lastSavedId); + dashboardBackup.clearState(lastSavedId); return { redirectRequired: true, id: newId }; } else { dashboardContentManagementCache.deleteDashboard(newId); // something changed in an existing dashboard, so delete it from the cache so that it can be re-fetched diff --git a/src/plugins/dashboard/public/services/dashboard_content_management/types.ts b/src/plugins/dashboard/public/services/dashboard_content_management/types.ts index 7eb9a0114bfe..e562f5d0cc28 100644 --- a/src/plugins/dashboard/public/services/dashboard_content_management/types.ts +++ b/src/plugins/dashboard/public/services/dashboard_content_management/types.ts @@ -23,7 +23,7 @@ import { DashboardCrudTypes } from '../../../common/content_management'; import { DashboardScreenshotModeService } from '../screenshot_mode/types'; import { DashboardInitializerContextService } from '../initializer_context/types'; import { DashboardSavedObjectsTaggingService } from '../saved_objects_tagging/types'; -import { DashboardSessionStorageServiceType } from '../dashboard_session_storage/types'; +import { DashboardBackupServiceType } from '../dashboard_backup/types'; import { DashboardDuplicateTitleCheckProps } from './lib/check_for_duplicate_dashboard_title'; export interface DashboardContentManagementRequiredServices { @@ -31,10 +31,10 @@ export interface DashboardContentManagementRequiredServices { spaces: DashboardSpacesService; embeddable: DashboardEmbeddableService; notifications: DashboardNotificationsService; + dashboardBackup: DashboardBackupServiceType; screenshotMode: DashboardScreenshotModeService; initializerContext: DashboardInitializerContextService; savedObjectsTagging: DashboardSavedObjectsTaggingService; - dashboardSessionStorage: DashboardSessionStorageServiceType; } export interface DashboardContentManagementService { @@ -63,6 +63,7 @@ type DashboardResolveMeta = DashboardCrudTypes['GetOut']['meta']; export interface LoadDashboardReturn { dashboardFound: boolean; + newDashboardCreated?: boolean; dashboardId?: string; managed?: boolean; resolveMeta?: DashboardResolveMeta; diff --git a/src/plugins/dashboard/public/services/plugin_services.stub.ts b/src/plugins/dashboard/public/services/plugin_services.stub.ts index 8ba55d486d75..b77888f1293f 100644 --- a/src/plugins/dashboard/public/services/plugin_services.stub.ts +++ b/src/plugins/dashboard/public/services/plugin_services.stub.ts @@ -19,7 +19,7 @@ import { applicationServiceFactory } from './application/application.stub'; import { chromeServiceFactory } from './chrome/chrome.stub'; import { coreContextServiceFactory } from './core_context/core_context.stub'; import { dashboardCapabilitiesServiceFactory } from './dashboard_capabilities/dashboard_capabilities.stub'; -import { dashboardSessionStorageServiceFactory } from './dashboard_session_storage/dashboard_session_storage.stub'; +import { dashboardBackupServiceFactory } from './dashboard_backup/dashboard_backup.stub'; import { dataServiceFactory } from './data/data.stub'; import { dataViewEditorServiceFactory } from './data_view_editor/data_view_editor.stub'; import { documentationLinksServiceFactory } from './documentation_links/documentation_links.stub'; @@ -51,7 +51,7 @@ export const providers: PluginServiceProviders = { chrome: new PluginServiceProvider(chromeServiceFactory), coreContext: new PluginServiceProvider(coreContextServiceFactory), dashboardCapabilities: new PluginServiceProvider(dashboardCapabilitiesServiceFactory), - dashboardSessionStorage: new PluginServiceProvider(dashboardSessionStorageServiceFactory), + dashboardBackup: new PluginServiceProvider(dashboardBackupServiceFactory), data: new PluginServiceProvider(dataServiceFactory), dataViewEditor: new PluginServiceProvider(dataViewEditorServiceFactory), documentationLinks: new PluginServiceProvider(documentationLinksServiceFactory), diff --git a/src/plugins/dashboard/public/services/plugin_services.ts b/src/plugins/dashboard/public/services/plugin_services.ts index f16b4c8f34b0..1d159014a4e7 100644 --- a/src/plugins/dashboard/public/services/plugin_services.ts +++ b/src/plugins/dashboard/public/services/plugin_services.ts @@ -19,7 +19,7 @@ import { applicationServiceFactory } from './application/application_service'; import { chromeServiceFactory } from './chrome/chrome_service'; import { coreContextServiceFactory } from './core_context/core_context_service'; import { dashboardCapabilitiesServiceFactory } from './dashboard_capabilities/dashboard_capabilities_service'; -import { dashboardSessionStorageServiceFactory } from './dashboard_session_storage/dashboard_session_storage_service'; +import { dashboardBackupServiceFactory } from './dashboard_backup/dashboard_backup_service'; import { dataServiceFactory } from './data/data_service'; import { dataViewEditorServiceFactory } from './data_view_editor/data_view_editor_service'; import { documentationLinksServiceFactory } from './documentation_links/documentation_links_service'; @@ -47,16 +47,16 @@ import { noDataPageServiceFactory } from './no_data_page/no_data_page_service'; const providers: PluginServiceProviders = { dashboardContentManagement: new PluginServiceProvider(dashboardContentManagementServiceFactory, [ - 'dashboardSessionStorage', 'savedObjectsTagging', 'initializerContext', + 'dashboardBackup', 'screenshotMode', 'notifications', 'embeddable', 'spaces', 'data', ]), - dashboardSessionStorage: new PluginServiceProvider(dashboardSessionStorageServiceFactory, [ + dashboardBackup: new PluginServiceProvider(dashboardBackupServiceFactory, [ 'notifications', 'spaces', ]), diff --git a/src/plugins/dashboard/public/services/types.ts b/src/plugins/dashboard/public/services/types.ts index 5ad3aab95112..c1c7c1aa39e7 100644 --- a/src/plugins/dashboard/public/services/types.ts +++ b/src/plugins/dashboard/public/services/types.ts @@ -19,7 +19,7 @@ import { DashboardCoreContextService } from './core_context/types'; import { DashboardCustomBrandingService } from './custom_branding/types'; import { DashboardCapabilitiesService } from './dashboard_capabilities/types'; import { DashboardContentManagementService } from './dashboard_content_management/types'; -import { DashboardSessionStorageServiceType } from './dashboard_session_storage/types'; +import { DashboardBackupServiceType } from './dashboard_backup/types'; import { DashboardDataService } from './data/types'; import { DashboardDataViewEditorService } from './data_view_editor/types'; import { DashboardDocumentationLinksService } from './documentation_links/types'; @@ -44,7 +44,7 @@ export type DashboardPluginServiceParams = KibanaPluginServiceParams { const actualTitle = await this.globalNav.getLastBreadcrumb(); this.log.debug(`Expected dashboard title ${expectedTitle}, actual: ${actualTitle}`); - return actualTitle === expectedTitle; + return actualTitle === expectedTitle || actualTitle === `Editing ${expectedTitle}`; } ); }