[Dashboard] Store view mode in local storage (#166523)

Moves the Dashboard view mode from session storage to local storage. This means that users will only need to enter edit mode **once** if they are an editor, and any subsequent Dashboards they open will already be in edit mode.
This commit is contained in:
Devon Thomson 2023-09-29 19:46:18 -04:00 committed by GitHub
parent 7e32fc8432
commit 8ffa8d8ee4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 223 additions and 110 deletions

View file

@ -70,9 +70,7 @@ describe('ShowShareModal', () => {
const getPropsAndShare = (
unsavedState?: Partial<DashboardContainerInput>
): ShowShareModalProps => {
pluginServices.getServices().dashboardSessionStorage.getState = jest
.fn()
.mockReturnValue(unsavedState);
pluginServices.getServices().dashboardBackup.getState = jest.fn().mockReturnValue(unsavedState);
return {
isDirty: true,
anchorElement: document.createElement('div'),

View file

@ -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 = {

View file

@ -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,
]);

View file

@ -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<DashboardContainerInput, 'id'> = {
viewMode: ViewMode.EDIT, // new dashboards start in edit mode.
viewMode: ViewMode.VIEW,
timeRestore: false,
query: { query: '', language: 'kuery' },
description: '',

View file

@ -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}',

View file

@ -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(

View file

@ -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<DashboardContainerInput> | 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,

View file

@ -402,13 +402,13 @@ export class DashboardContainer extends Container<InheritedChildInput, Dashboard
this.searchSessionId = searchSessionId;
this.updateInput(newInput);
batch(() => {
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);
};

View file

@ -211,8 +211,8 @@ function backupUnsavedChanges(
this: DashboardContainer,
unsavedChanges: Partial<DashboardContainerInput>
) {
const { dashboardSessionStorage } = pluginServices.getServices();
dashboardSessionStorage.setState(
const { dashboardBackup } = pluginServices.getServices();
dashboardBackup.setState(
this.getDashboardSavedObjectId(),
omit(unsavedChanges, keysToOmitFromSessionStorage)
);

View file

@ -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,
]);

View file

@ -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);
});
});

View file

@ -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 : (
<>

View file

@ -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;

View file

@ -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<string>();
const [hasInitialFetchReturned, setHasInitialFetchReturned] = useState(false);
const [unsavedDashboardIds, setUnsavedDashboardIds] = useState<string[]>(
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<DashboardContainerInput, 'id' | 'title' | 'description' | 'tags'>) => {
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 {

View file

@ -7,16 +7,17 @@
*/
import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public';
import { DashboardSessionStorageServiceType } from './types';
import { DashboardBackupServiceType } from './types';
type DashboardSessionStorageServiceFactory =
PluginServiceFactory<DashboardSessionStorageServiceType>;
type DashboardBackupServiceFactory = PluginServiceFactory<DashboardBackupServiceType>;
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']),

View file

@ -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);
};

View file

@ -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<DashboardContainerInput> | undefined;
setState: (id: string | undefined, newState: Partial<DashboardContainerInput>) => void;
getViewMode: () => ViewMode;
storeViewMode: (viewMode: ViewMode) => void;
getDashboardIdsWithUnsavedChanges: () => string[];
dashboardHasUnsavedEdits: (id?: string) => boolean;
}

View file

@ -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 }) =>

View file

@ -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

View file

@ -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<SaveDashboardReturn> => {
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

View file

@ -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;

View file

@ -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<DashboardServices> = {
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),

View file

@ -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<DashboardServices, DashboardPluginServiceParams> = {
dashboardContentManagement: new PluginServiceProvider(dashboardContentManagementServiceFactory, [
'dashboardSessionStorage',
'savedObjectsTagging',
'initializerContext',
'dashboardBackup',
'screenshotMode',
'notifications',
'embeddable',
'spaces',
'data',
]),
dashboardSessionStorage: new PluginServiceProvider(dashboardSessionStorageServiceFactory, [
dashboardBackup: new PluginServiceProvider(dashboardBackupServiceFactory, [
'notifications',
'spaces',
]),

View file

@ -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<DashboardSt
initContext: PluginInitializerContext; // need a custom type so that initContext is a required parameter for initializerContext
};
export interface DashboardServices {
dashboardSessionStorage: DashboardSessionStorageServiceType;
dashboardBackup: DashboardBackupServiceType;
dashboardContentManagement: DashboardContentManagementService;
analytics: DashboardAnalyticsService;

View file

@ -38,6 +38,24 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await kibanaServer.savedObjects.cleanStandardList();
});
it('existing dashboard opens in last used view mode', async function () {
await PageObjects.dashboard.gotoDashboardLandingPage();
await PageObjects.dashboard.loadSavedDashboard(dashboardName);
expect(await PageObjects.dashboard.getIsInViewMode()).to.equal(true);
await PageObjects.dashboard.switchToEditMode();
await PageObjects.dashboard.gotoDashboardLandingPage();
await PageObjects.dashboard.loadSavedDashboard(dashboardName);
expect(await PageObjects.dashboard.getIsInViewMode()).to.equal(false);
await PageObjects.dashboard.gotoDashboardLandingPage();
await PageObjects.dashboard.loadSavedDashboard('few panels');
expect(await PageObjects.dashboard.getIsInViewMode()).to.equal(false);
await PageObjects.dashboard.clickCancelOutOfEditMode();
});
it('create new dashboard opens in edit mode', async function () {
await PageObjects.dashboard.gotoDashboardLandingPage();
await PageObjects.dashboard.clickNewDashboard();
@ -45,14 +63,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
expect(isInViewMode).to.be(false);
});
it('existing dashboard opens in view mode', async function () {
await PageObjects.dashboard.gotoDashboardLandingPage();
await PageObjects.dashboard.loadSavedDashboard(dashboardName);
const inViewMode = await PageObjects.dashboard.getIsInViewMode();
expect(inViewMode).to.equal(true);
});
describe('save', function () {
it('auto exits out of edit mode', async function () {
await PageObjects.dashboard.gotoDashboardEditMode(dashboardName);

View file

@ -182,11 +182,11 @@ export class DashboardPageObject extends FtrService {
public async expectOnDashboard(expectedTitle: string) {
await this.retry.waitFor(
`last breadcrumb to have dashboard title: ${expectedTitle}`,
`last breadcrumb to have dashboard title: ${expectedTitle} OR Editing ${expectedTitle}`,
async () => {
const actualTitle = await this.globalNav.getLastBreadcrumb();
this.log.debug(`Expected dashboard title ${expectedTitle}, actual: ${actualTitle}`);
return actualTitle === expectedTitle;
return actualTitle === expectedTitle || actualTitle === `Editing ${expectedTitle}`;
}
);
}